Generate render tool.
This commit is contained in:
parent
7bd041a02c
commit
f4470c9625
7
extensions/dicom-sr/src/constants/scoordTypes.js
Normal file
7
extensions/dicom-sr/src/constants/scoordTypes.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
POINT: 'POINT',
|
||||||
|
MULTIPOINT: 'MULTIPOINT',
|
||||||
|
POLYLINE: 'POLYLINE',
|
||||||
|
CIRCLE: 'CIRCLE',
|
||||||
|
ELLIPSE: 'ELLIPSE',
|
||||||
|
};
|
||||||
5
extensions/dicom-sr/src/constants/toolNames.js
Normal file
5
extensions/dicom-sr/src/constants/toolNames.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const TOOL_NAMES = {
|
||||||
|
DICOM_SR_DISPLAY_TOOL: 'DICOMSRDisplayTool',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TOOL_NAMES;
|
||||||
@ -1,5 +1,8 @@
|
|||||||
import id from './id';
|
import id from './id';
|
||||||
import { utils } from '@ohif/core';
|
import { utils, classes } from '@ohif/core';
|
||||||
|
import addMeasurement from './utils/addMeasurement.js';
|
||||||
|
|
||||||
|
const { ImageSet } = classes;
|
||||||
|
|
||||||
const sopClassHandlerName = 'dicom-sr';
|
const sopClassHandlerName = 'dicom-sr';
|
||||||
|
|
||||||
@ -9,8 +12,6 @@ const sopClassUids = [
|
|||||||
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
|
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
|
||||||
];
|
];
|
||||||
|
|
||||||
const scoordTypes = ['POINT', 'MULTIPOINT', 'POLYLINE', 'CIRCLE', 'ELLIPSE'];
|
|
||||||
|
|
||||||
const CodeNameCodeSequenceValues = {
|
const CodeNameCodeSequenceValues = {
|
||||||
ImagingMeasurementReport: '126000',
|
ImagingMeasurementReport: '126000',
|
||||||
ImageLibrary: '111028',
|
ImageLibrary: '111028',
|
||||||
@ -20,6 +21,10 @@ const CodeNameCodeSequenceValues = {
|
|||||||
TrackingUniqueIdentifier: '112040',
|
TrackingUniqueIdentifier: '112040',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RELATIONSHIP_TYPE = {
|
||||||
|
INFERRED_FROM: 'INFERRED FROM',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic SOPClassHandler:
|
* Basic SOPClassHandler:
|
||||||
* - For all Image types that are stackable, create
|
* - For all Image types that are stackable, create
|
||||||
@ -29,16 +34,23 @@ const CodeNameCodeSequenceValues = {
|
|||||||
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
|
||||||
* @returns {Array} The list of display sets created for the given series object
|
* @returns {Array} The list of display sets created for the given series object
|
||||||
*/
|
*/
|
||||||
function getDisplaySetsFromSeries(instances) {
|
function _getDisplaySetsFromSeries(
|
||||||
|
instances,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager
|
||||||
|
) {
|
||||||
// If the series has no instances, stop here
|
// If the series has no instances, stop here
|
||||||
if (!instances || !instances.length) {
|
if (!instances || !instances.length) {
|
||||||
throw new Error('No instances were provided');
|
throw new Error('No instances were provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { DisplaySetService } = servicesManager.services;
|
||||||
|
const dataSources = extensionManager.getDataSources();
|
||||||
|
const dataSource = dataSources[0];
|
||||||
|
|
||||||
const instance = instances[0];
|
const instance = instances[0];
|
||||||
|
|
||||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
|
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
|
||||||
|
|
||||||
const { ConceptNameCodeSequence, ContentSequence } = instance;
|
const { ConceptNameCodeSequence, ContentSequence } = instance;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -51,10 +63,6 @@ function getDisplaySetsFromSeries(instances) {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const referencedImages = _getReferencedImagesList(ContentSequence);
|
|
||||||
|
|
||||||
const measurements = _getMeasurements(ContentSequence, SOPInstanceUID);
|
|
||||||
|
|
||||||
const displaySet = {
|
const displaySet = {
|
||||||
plugin: id,
|
plugin: id,
|
||||||
Modality: 'SR',
|
Modality: 'SR',
|
||||||
@ -63,13 +71,141 @@ function getDisplaySetsFromSeries(instances) {
|
|||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
||||||
referencedImages,
|
referencedImages: _getReferencedImagesList(ContentSequence),
|
||||||
|
measurements: _getMeasurements(ContentSequence),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(DisplaySetService);
|
||||||
|
|
||||||
|
// Check currently added displaySets and add measurements if the sources exist.
|
||||||
|
DisplaySetService.activeDisplaySets.forEach(activeDisplaySet => {
|
||||||
|
_checkIfCanAddMeasurementsToDisplaySet(
|
||||||
|
displaySet,
|
||||||
|
activeDisplaySet,
|
||||||
|
dataSource
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to new displaySets as the source may come in after.
|
||||||
|
DisplaySetService.subscribe(
|
||||||
|
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||||
|
newDisplaySets => {
|
||||||
|
// If there are still some measurements that have not yet been loaded into cornerstone,
|
||||||
|
// See if we can load them onto any of the new displaySets.
|
||||||
|
newDisplaySets.forEach(newDisplaySet => {
|
||||||
|
_checkIfCanAddMeasurementsToDisplaySet(
|
||||||
|
displaySet,
|
||||||
|
newDisplaySet,
|
||||||
|
dataSource
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return [displaySet];
|
return [displaySet];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSopClassHandlerModule() {
|
function _checkIfCanAddMeasurementsToDisplaySet(
|
||||||
|
srDisplaySet,
|
||||||
|
newDisplaySet,
|
||||||
|
dataSource
|
||||||
|
) {
|
||||||
|
let unloadedMeasurements = srDisplaySet.measurements.filter(
|
||||||
|
measurement => measurement.loaded === false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unloadedMeasurements.length === 0) {
|
||||||
|
// All already loaded!
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newDisplaySet instanceof ImageSet) {
|
||||||
|
// This also filters out _this_ displaySet, as it is not an ImageSet.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sopClassUids, images } = newDisplaySet;
|
||||||
|
|
||||||
|
// Check if any have the newDisplaySet is the correct SOPClass.
|
||||||
|
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
|
||||||
|
measurement.coords.some(coord =>
|
||||||
|
sopClassUids.includes(coord.ReferencedSOPSequence.ReferencedSOPClassUID)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (unloadedMeasurements.length === 0) {
|
||||||
|
// New displaySet isn't the correct SOPClass, so can't contain the referenced images.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SOPInstanceUIDs = [];
|
||||||
|
|
||||||
|
unloadedMeasurements.forEach(measurement => {
|
||||||
|
const { coords } = measurement;
|
||||||
|
|
||||||
|
coords.forEach(coord => {
|
||||||
|
const SOPInstanceUID =
|
||||||
|
coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||||
|
|
||||||
|
if (!SOPInstanceUIDs.includes(SOPInstanceUID)) {
|
||||||
|
SOPInstanceUIDs.push(SOPInstanceUID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(
|
||||||
|
newDisplaySet
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < images.length; i++) {
|
||||||
|
if (!unloadedMeasurements.length) {
|
||||||
|
// All measurements loaded.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = images[i];
|
||||||
|
const { SOPInstanceUID } = image;
|
||||||
|
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
|
||||||
|
const imageId = imageIdsForDisplaySet[i];
|
||||||
|
|
||||||
|
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
|
||||||
|
const measurement = unloadedMeasurements[j];
|
||||||
|
if (_measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID)) {
|
||||||
|
addMeasurement(
|
||||||
|
measurement,
|
||||||
|
imageId,
|
||||||
|
newDisplaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
unloadedMeasurements.splice(j, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _measurmentReferencesSOPInstanceUID(measurement, SOPInstanceUID) {
|
||||||
|
const { coords } = measurement;
|
||||||
|
|
||||||
|
for (let j = 0; j < coords.length; j++) {
|
||||||
|
const coord = coords[j];
|
||||||
|
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;
|
||||||
|
|
||||||
|
if (ReferencedSOPInstanceUID === SOPInstanceUID) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
|
||||||
|
const getDisplaySetsFromSeries = instances => {
|
||||||
|
return _getDisplaySetsFromSeries(
|
||||||
|
instances,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: sopClassHandlerName,
|
name: sopClassHandlerName,
|
||||||
@ -79,10 +215,7 @@ function getSopClassHandlerModule() {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getMeasurements(
|
function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
||||||
ImagingMeasurementReportContentSequence,
|
|
||||||
SOPInstanceUID
|
|
||||||
) {
|
|
||||||
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
|
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
|
||||||
item =>
|
item =>
|
||||||
item.ConceptNameCodeSequence.CodeValue ===
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
@ -101,7 +234,7 @@ function _getMeasurements(
|
|||||||
MeasurementGroups
|
MeasurementGroups
|
||||||
);
|
);
|
||||||
|
|
||||||
let allMeasurements = [];
|
let measurements = [];
|
||||||
|
|
||||||
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
|
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
|
||||||
trackingUniqueIdentifier => {
|
trackingUniqueIdentifier => {
|
||||||
@ -110,13 +243,15 @@ function _getMeasurements(
|
|||||||
trackingUniqueIdentifier
|
trackingUniqueIdentifier
|
||||||
];
|
];
|
||||||
|
|
||||||
const measurements = _processMeasurement(mergedContentSequence);
|
const measurement = _processMeasurement(mergedContentSequence);
|
||||||
|
|
||||||
if (measurements) {
|
if (measurement) {
|
||||||
allMeasurements = allMeasurements.concat(measurements);
|
measurements.push(measurement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return measurements;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getMergedContentSequencesByTrackingUniqueIdentifiers(
|
function _getMergedContentSequencesByTrackingUniqueIdentifiers(
|
||||||
@ -172,8 +307,6 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _processMeasurement(mergedContentSequence) {
|
function _processMeasurement(mergedContentSequence) {
|
||||||
debugger;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
mergedContentSequence.some(
|
mergedContentSequence.some(
|
||||||
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
|
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
|
||||||
@ -182,34 +315,134 @@ function _processMeasurement(mergedContentSequence) {
|
|||||||
return _processTID1410Measurement(mergedContentSequence);
|
return _processTID1410Measurement(mergedContentSequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _processNonGeometricallyDefinedMeasurement(mergedContentSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _processTID1410Measurement(mergedContentSequence) {
|
||||||
|
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
|
||||||
|
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
|
||||||
|
// TODO -> Look at RelationshipType => Contains means
|
||||||
|
|
||||||
|
const graphicItem = mergedContentSequence.find(
|
||||||
|
group => group.ValueType === 'SCOORD'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!graphicItem) {
|
||||||
|
console.warn(
|
||||||
|
`graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const NUMContentItems = mergedContentSequence.filter(
|
const NUMContentItems = mergedContentSequence.filter(
|
||||||
group => group.ValueType === 'NUM'
|
group => group.ValueType === 'NUM'
|
||||||
);
|
);
|
||||||
|
|
||||||
const measurements = [];
|
const measurement = {
|
||||||
|
loaded: false,
|
||||||
|
labels: [],
|
||||||
|
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
|
||||||
|
};
|
||||||
|
|
||||||
NUMContentItems.forEach(item => {
|
NUMContentItems.forEach(item => {
|
||||||
debugger;
|
const { ConceptNameCodeSequence, MeasuredValueSequence } = item;
|
||||||
|
|
||||||
const { ContentSequence, MeasuredValueSequence } = item;
|
if (MeasuredValueSequence) {
|
||||||
|
measurement.labels.push(
|
||||||
// GET -> REPRESENTATION, REFERNECED IMAGE, measurement value
|
_getLabelFromMeasuredValueSequence(
|
||||||
|
ConceptNameCodeSequence,
|
||||||
|
MeasuredValueSequence
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
|
return measurement;
|
||||||
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
|
|
||||||
|
|
||||||
// TODO -> Look at RelationshipType => Contains means
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function _processTID1410Measurement(mergedContentSequence) {
|
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||||
// TODO
|
const NUMContentItems = mergedContentSequence.filter(
|
||||||
console.error('TODO => Process TID1410Measurement');
|
group => group.ValueType === 'NUM'
|
||||||
debugger;
|
);
|
||||||
|
|
||||||
return [ß];
|
const measurement = {
|
||||||
|
loaded: false,
|
||||||
|
labels: [],
|
||||||
|
coords: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
NUMContentItems.forEach(item => {
|
||||||
|
const {
|
||||||
|
ConceptNameCodeSequence,
|
||||||
|
ContentSequence,
|
||||||
|
MeasuredValueSequence,
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
const { ValueType } = ContentSequence;
|
||||||
|
|
||||||
|
if (!ValueType === 'SCOORD') {
|
||||||
|
console.warn(
|
||||||
|
`Graphic ${ValueType} not currently supported, skipping annotation.`
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
|
||||||
|
|
||||||
|
if (coords) {
|
||||||
|
measurement.coords.push(coords);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MeasuredValueSequence) {
|
||||||
|
measurement.labels.push(
|
||||||
|
_getLabelFromMeasuredValueSequence(
|
||||||
|
ConceptNameCodeSequence,
|
||||||
|
MeasuredValueSequence
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return measurement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getCoordsFromSCOORDOrSCOORD3D(item) {
|
||||||
|
const { ValueType, RelationshipType, GraphicType, GraphicData } = item;
|
||||||
|
|
||||||
|
if (RelationshipType !== RELATIONSHIP_TYPE.INFERRED_FROM) {
|
||||||
|
console.warn(
|
||||||
|
`Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM."`
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coords = { ValueType, GraphicType, GraphicData };
|
||||||
|
|
||||||
|
// ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM'
|
||||||
|
if (ValueType === 'SCOORD') {
|
||||||
|
const { ReferencedSOPSequence } = item.ContentSequence;
|
||||||
|
|
||||||
|
coords.ReferencedSOPSequence = ReferencedSOPSequence;
|
||||||
|
} else if (ValueType === 'SCOORD3D') {
|
||||||
|
const { ReferencedFrameOfReferenceSequence } = item.ContentSequence;
|
||||||
|
|
||||||
|
coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getLabelFromMeasuredValueSequence(
|
||||||
|
ConceptNameCodeSequence,
|
||||||
|
MeasuredValueSequence
|
||||||
|
) {
|
||||||
|
const { CodeMeaning } = ConceptNameCodeSequence;
|
||||||
|
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
|
||||||
|
const { CodeValue } = MeasurementUnitsCodeSequence;
|
||||||
|
|
||||||
|
return { label: CodeMeaning, value: `${NumericValue} ${CodeValue}` }; // E.g. Long Axis: 31.0 mm
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
import getSopClassHandlerModule from './getSopClassHandlerModule';
|
||||||
import id from './id.js';
|
import id from './id.js';
|
||||||
|
import init from './init';
|
||||||
|
|
||||||
const Component = React.lazy(() => {
|
const Component = React.lazy(() => {
|
||||||
return import('./OHIFCornerstoneSRViewport');
|
return import('./OHIFCornerstoneSRViewport');
|
||||||
@ -23,6 +24,10 @@ export default {
|
|||||||
*/
|
*/
|
||||||
id,
|
id,
|
||||||
|
|
||||||
|
preRegistration({ servicesManager, configuration = {} }) {
|
||||||
|
init({ servicesManager, configuration });
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
|
|||||||
21
extensions/dicom-sr/src/init.js
Normal file
21
extensions/dicom-sr/src/init.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||||
|
|
||||||
|
import TOOL_NAMES from './constants/toolNames';
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
TOOL_NAMES: {
|
||||||
|
DICOM_SR_DISPLAY_TOOL: 'DICOMSRDisplayTool',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} configuration
|
||||||
|
*/
|
||||||
|
export default function init({ configuration = {} }) {
|
||||||
|
const conifg = Object.assign({}, defaultConfig, configuration);
|
||||||
|
|
||||||
|
TOOL_NAMES.DICOM_SR_DISPLAY_TOOL = conifg.TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
|
||||||
|
|
||||||
|
cornerstoneTools.addTool(DICOMSRDisplayTool);
|
||||||
|
}
|
||||||
123
extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js
Normal file
123
extensions/dicom-sr/src/tools/DICOMSRDisplayTool.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
|
||||||
|
|
||||||
|
import TOOL_NAMES from '../constants/toolNames';
|
||||||
|
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||||
|
|
||||||
|
// Cornerstone 3rd party dev kit imports
|
||||||
|
const draw = importInternal('drawing/draw');
|
||||||
|
const drawJoinedLines = importInternal('drawing/drawJoinedLines');
|
||||||
|
const drawCircle = importInternal('drawing/drawCircle');
|
||||||
|
const drawEllipse = importInternal('drawing/drawEllipse');
|
||||||
|
const getNewContext = importInternal('drawing/getNewContext');
|
||||||
|
const BaseTool = importInternal('base/BaseTool');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class DICOMSRDisplayTool - Renders DICOMSR data in a read only manner (i.e. as an overlay).
|
||||||
|
* @extends cornerstoneTools.BaseTool
|
||||||
|
*/
|
||||||
|
export default class DICOMSRDisplayTool extends BaseTool {
|
||||||
|
constructor(props = {}) {
|
||||||
|
const defaultProps = {
|
||||||
|
mixins: ['enabledOrDisabledBinaryTool'],
|
||||||
|
name: TOOL_NAMES.DICOM_SR_DISPLAY_TOOL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialProps = Object.assign(defaultProps, props);
|
||||||
|
|
||||||
|
super(initialProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToolData(evt) {
|
||||||
|
const eventData = evt.detail;
|
||||||
|
const { element } = eventData;
|
||||||
|
|
||||||
|
const toolState = getToolState(element, this.name);
|
||||||
|
|
||||||
|
if (!toolState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
color: toolColors.setToolColor(),
|
||||||
|
lineWidth: 2,
|
||||||
|
handleRadius: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < toolState.data.length; i++) {
|
||||||
|
const data = toolState.data[i];
|
||||||
|
|
||||||
|
Object.keys(data).forEach(GraphicType => {
|
||||||
|
const renderableData = data[GraphicType];
|
||||||
|
|
||||||
|
switch (GraphicType) {
|
||||||
|
case SCOORD_TYPES.POINT:
|
||||||
|
case SCOORD_TYPES.MULTIPOINT:
|
||||||
|
renderPointOrMultipoint(renderableData, eventData, options);
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.POLYLINE:
|
||||||
|
renderPolyLine(renderableData, eventData, options);
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.CIRCLE:
|
||||||
|
renderCircle(renderableData, eventData, options);
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.ELLIPSE:
|
||||||
|
renderEllipse(renderableData, eventData, options);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPolyLine(renderableData, eventData, options) {
|
||||||
|
const { element } = eventData;
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
renderableData.forEach(points => {
|
||||||
|
draw(context, context => {
|
||||||
|
drawJoinedLines(context, element, points[0], points, options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPointOrMultipoint(renderableData, eventData, options) {
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
renderableData.forEach(points => {
|
||||||
|
draw(context, context => {
|
||||||
|
drawHandles(context, eventData, points, options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCircle(renderableData, eventData, options) {
|
||||||
|
const { element } = eventData;
|
||||||
|
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
renderableData.forEach(circle => {
|
||||||
|
const { center, radius } = circle;
|
||||||
|
|
||||||
|
drawCircle(context, element, center, radius, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEllipse(renderableData, eventData, options) {
|
||||||
|
const { element } = eventData;
|
||||||
|
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
renderableData.forEach(ellipse => {
|
||||||
|
const { corner1, corner2 } = ellipse;
|
||||||
|
|
||||||
|
drawEllipse(
|
||||||
|
context,
|
||||||
|
element,
|
||||||
|
corner1,
|
||||||
|
corner2,
|
||||||
|
options,
|
||||||
|
'pixel',
|
||||||
|
0 // TODO -> Work our the initial rotation and add it here so we render appropriately rotated ellipses.
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
135
extensions/dicom-sr/src/utils/addMeasurement.js
Normal file
135
extensions/dicom-sr/src/utils/addMeasurement.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import TOOL_NAMES from '../constants/toolNames';
|
||||||
|
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||||
|
import { cornerstone } from '../../../../platform/core/src';
|
||||||
|
|
||||||
|
const globalImageIdSpecificToolStateManager =
|
||||||
|
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
|
||||||
|
export default function addMeasurement(
|
||||||
|
measurement,
|
||||||
|
imageId,
|
||||||
|
displaySetInstanceUID
|
||||||
|
) {
|
||||||
|
console.log('== ADD MEASUREMENT TO CST ==');
|
||||||
|
console.log(measurement);
|
||||||
|
console.log(imageId);
|
||||||
|
console.log('============================');
|
||||||
|
|
||||||
|
// TODO -> GET TRACKING UID AND ADD IT TO TOOL DATA AND ALSO THE MEASUREMENT ENTRY.
|
||||||
|
// TODO -> Render rotated ellipse .
|
||||||
|
|
||||||
|
debugger;
|
||||||
|
|
||||||
|
const toolName = TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
|
||||||
|
|
||||||
|
const measurementData = {};
|
||||||
|
|
||||||
|
measurement.coords.forEach(coord => {
|
||||||
|
const { GraphicType, GraphicData } = coord;
|
||||||
|
|
||||||
|
if (measurementData[GraphicType] === undefined) {
|
||||||
|
measurementData[GraphicType] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderableData = _getRenderableData(GraphicType, GraphicData);
|
||||||
|
|
||||||
|
measurementData[GraphicType].push(renderableData);
|
||||||
|
});
|
||||||
|
|
||||||
|
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
|
||||||
|
if (toolState[imageId] === undefined) {
|
||||||
|
toolState[imageId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIdToolState = toolState[imageId];
|
||||||
|
|
||||||
|
// If we don't have tool state for this type of tool, add an empty object
|
||||||
|
if (imageIdToolState[toolName] === undefined) {
|
||||||
|
imageIdToolState[toolName] = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolData = imageIdToolState[toolName];
|
||||||
|
|
||||||
|
toolData.data.push(measurementData);
|
||||||
|
|
||||||
|
measurement.loaded = true;
|
||||||
|
measurement.imageId = imageId;
|
||||||
|
measurement.displaySetInstanceUID = displaySetInstanceUID;
|
||||||
|
debugger;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getRenderableData(GraphicType, GraphicData) {
|
||||||
|
let renderableData;
|
||||||
|
|
||||||
|
switch (GraphicType) {
|
||||||
|
case SCOORD_TYPES.POINT:
|
||||||
|
case SCOORD_TYPES.MULTIPOINT:
|
||||||
|
case SCOORD_TYPES.POLYLINE:
|
||||||
|
renderableData = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||||
|
renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.CIRCLE:
|
||||||
|
const center = { x: GraphicData[0], y: GraphicData[1] };
|
||||||
|
const onPerimeter = { x: GraphicData[2], y: GraphicData[3] };
|
||||||
|
|
||||||
|
const radius = cornerstoneMath.point.distance(center, onPerimeter);
|
||||||
|
|
||||||
|
renderableData = {
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.ELLIPSE:
|
||||||
|
console.warn('ROTATED ELLIPSE NOT YET SUPPORTED!');
|
||||||
|
|
||||||
|
const majorAxis = [
|
||||||
|
{ x: GraphicData[0], y: GraphicData[1] },
|
||||||
|
{ x: GraphicData[2], y: GraphicData[3] },
|
||||||
|
];
|
||||||
|
const minorAxis = [
|
||||||
|
{ x: GraphicData[4], y: GraphicData[5] },
|
||||||
|
{ x: GraphicData[6], y: GraphicData[7] },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Calculate two opposite corners of box defined by two axes.
|
||||||
|
|
||||||
|
const minorAxisLength = cornerstoneMath.point.distance(
|
||||||
|
minorAxis[0],
|
||||||
|
minorAxis[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
const minorAxisDirection = {
|
||||||
|
x: (minorAxis[1].x - minorAxis[0].x) / minorAxisLength,
|
||||||
|
y: (minorAxis[1].y - minorAxis[0].y) / minorAxisLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
const halfMinorAxisLength = minorAxisLength / 2;
|
||||||
|
|
||||||
|
// First end point of major axis + half minor axis vector
|
||||||
|
const corner1 = {
|
||||||
|
x: majorAxis[0].x + minorAxisDirection.x * halfMinorAxisLength,
|
||||||
|
y: majorAxis[0].y + minorAxisDirection.y * halfMinorAxisLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Second end point of major axis - half of minor axis vector
|
||||||
|
const corner2 = {
|
||||||
|
x: majorAxis[1].x - minorAxisDirection.x * halfMinorAxisLength,
|
||||||
|
y: majorAxis[1].y - minorAxisDirection.y * halfMinorAxisLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderableData = {
|
||||||
|
corner1,
|
||||||
|
corner2,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderableData;
|
||||||
|
}
|
||||||
@ -17,9 +17,12 @@ export default class ExtensionManager {
|
|||||||
this.modules[moduleType] = [];
|
this.modules[moduleType] = [];
|
||||||
});
|
});
|
||||||
this.dataSourceMap = {};
|
this.dataSourceMap = {};
|
||||||
|
this.defaultDataSourceName = appConfig.defaultDataSourceName;
|
||||||
|
this.activeDataSource = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('modules map');
|
setActiveDataSource(dataSourceName) {
|
||||||
console.log(this.modulesMap);
|
this.activeDataSource = dataSourceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -137,6 +140,10 @@ export default class ExtensionManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getDataSources = dataSourceName => {
|
getDataSources = dataSourceName => {
|
||||||
|
if (dataSourceName === undefined) {
|
||||||
|
dataSourceName = this.activeDataSource;
|
||||||
|
}
|
||||||
|
|
||||||
// Note: this currently uses the data source name, which feels weird...
|
// Note: this currently uses the data source name, which feels weird...
|
||||||
return this.dataSourceMap[dataSourceName];
|
return this.dataSourceMap[dataSourceName];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -23,4 +23,5 @@ window.config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
defaultDataSourceName: 'dicomweb',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import {
|
|||||||
// redux as reduxOHIF,
|
// redux as reduxOHIF,
|
||||||
} from '@ohif/core';
|
} from '@ohif/core';
|
||||||
|
|
||||||
|
// TODO -> this feels bad.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration
|
* @param {object|func} appConfigOrFunc - application configuration, or a function that returns application configuration
|
||||||
* @param {object[]} defaultExtensions - array of extension objects
|
* @param {object[]} defaultExtensions - array of extension objects
|
||||||
|
|||||||
@ -27,12 +27,15 @@ export default function ModeRoute({
|
|||||||
: [StudyInstanceUIDs];
|
: [StudyInstanceUIDs];
|
||||||
|
|
||||||
const { extensions, sopClassHandlers } = mode;
|
const { extensions, sopClassHandlers } = mode;
|
||||||
// TODO:
|
|
||||||
// - Check query/params for specific dataSource
|
if (dataSourceName === undefined) {
|
||||||
// - If provided, query for that dataSource instance
|
dataSourceName = extensionManager.defaultDataSourceName;
|
||||||
// - If not provided, select default datasource
|
}
|
||||||
// - Update `extensionManager` to have a method to retrieve the default source
|
|
||||||
const dataSources = extensionManager.getDataSources(dataSourceName);
|
extensionManager.setActiveDataSource(dataSourceName);
|
||||||
|
|
||||||
|
const dataSources = extensionManager.getDataSources();
|
||||||
|
|
||||||
const dataSource = dataSources[0];
|
const dataSource = dataSources[0];
|
||||||
const route = mode.routes[0];
|
const route = mode.routes[0];
|
||||||
|
|
||||||
|
|||||||
@ -68,6 +68,30 @@ export default function buildModeRoutes(
|
|||||||
exact: true,
|
exact: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const defaultDataSourceName = extensionManager.defaultDataSourceName;
|
||||||
|
|
||||||
|
// Add default DataSource route.
|
||||||
|
const path = `/${mode.id}`;
|
||||||
|
|
||||||
|
// TODO move up.
|
||||||
|
const component = ({ location }) => (
|
||||||
|
<ViewModelProvider>
|
||||||
|
<ModeRoute
|
||||||
|
location={location}
|
||||||
|
mode={mode}
|
||||||
|
dataSourceName={defaultDataSourceName}
|
||||||
|
extensionManager={extensionManager}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
/>
|
||||||
|
</ViewModelProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
routes.push({
|
||||||
|
path,
|
||||||
|
component,
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
|
|||||||
@ -29,8 +29,7 @@ const createRoutes = (
|
|||||||
const routes =
|
const routes =
|
||||||
buildModeRoutes(modes, dataSources, extensionManager, servicesManager) ||
|
buildModeRoutes(modes, dataSources, extensionManager, servicesManager) ||
|
||||||
[];
|
[];
|
||||||
// TODO: Shouldn't need to guard input routes with an empty array?
|
const allRoutes = [...routes, ...bakedInRoutes];
|
||||||
const allRoutes = [...(routes || []), ...bakedInRoutes];
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'Creating Routes: ',
|
'Creating Routes: ',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user