Re-add extensions as top level dir
This commit is contained in:
1 parent
77ec71a12a
commit
780f84af89
86 files changed
+51115
No files matched your search
@@ -0,0 +1,11 @@
|
||||
:root {
|
||||
--text-color-primary: white;
|
||||
}
|
||||
|
||||
.DicomHtmlViewport {
|
||||
padding: 20px;
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: var(--text-color-primary);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import React, { Component } from 'react';
|
||||
import * as dcmjs from 'dcmjs';
|
||||
import TypedArrayProp from './TypedArrayProp';
|
||||
import './DicomHtmlViewport.css';
|
||||
|
||||
function getRelationshipString(data) {
|
||||
switch (data.RelationshipType) {
|
||||
case 'HAS CONCEPT MOD':
|
||||
return 'Concept modifier: ';
|
||||
case 'HAS OBS CONTEXT':
|
||||
return 'Observation context: ';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const getMeaningString = data => {
|
||||
if (data.ConceptNameCodeSequence) {
|
||||
const { CodeMeaning } = data.ConceptNameCodeSequence;
|
||||
|
||||
return `${CodeMeaning} = `;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
function getValueString(data) {
|
||||
switch (data.ValueType) {
|
||||
case 'CODE':
|
||||
const {
|
||||
CodeMeaning,
|
||||
CodeValue,
|
||||
CodingSchemeDesignator
|
||||
} = data.ConceptNameCodeSequence;
|
||||
|
||||
return `${CodeMeaning} (${CodeValue}, ${CodingSchemeDesignator})`;
|
||||
|
||||
case 'PNAME':
|
||||
return data.PersonName;
|
||||
|
||||
case 'TEXT':
|
||||
return data.TextValue;
|
||||
|
||||
case 'UIDREF':
|
||||
return data.UID;
|
||||
|
||||
case 'NUM':
|
||||
const { MeasuredValueSequence } = data;
|
||||
const numValue = MeasuredValueSequence.NumericValue;
|
||||
const codeValue =
|
||||
MeasuredValueSequence.MeasurementUnitsCodeSequence.CodeValue;
|
||||
return `${numValue} ${codeValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
function constructPlainValue(data) {
|
||||
const value = getValueString(data);
|
||||
|
||||
if (value) {
|
||||
return getRelationshipString(data) + getMeaningString(data) + value;
|
||||
}
|
||||
}
|
||||
|
||||
function constructContentSequence(data, header) {
|
||||
if (!data.ContentSequence) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = data.ContentSequence.map(item => parseContent(item)).filter(
|
||||
item => item
|
||||
);
|
||||
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = {
|
||||
items
|
||||
};
|
||||
|
||||
if (header) {
|
||||
result.header = header;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseContent(data) {
|
||||
if (data.ValueType) {
|
||||
if (data.ValueType === 'CONTAINER') {
|
||||
const header = data.ConceptNameCodeSequence.CodeMeaning;
|
||||
|
||||
return constructContentSequence(data, header);
|
||||
}
|
||||
|
||||
return constructPlainValue(data);
|
||||
}
|
||||
|
||||
if (data.ContentSequence) {
|
||||
return constructContentSequence(data);
|
||||
}
|
||||
}
|
||||
|
||||
const { DicomMetaDictionary, DicomMessage } = dcmjs.data;
|
||||
|
||||
function getMainData(data) {
|
||||
const root = [];
|
||||
|
||||
const patientValue = `${data.PatientName} (${data.PatientSex}, #${
|
||||
data.PatientID
|
||||
})`;
|
||||
root.push(getMainDataItem('Patient', patientValue));
|
||||
|
||||
const studyValue = data.StudyDescription;
|
||||
root.push(getMainDataItem('Study', studyValue));
|
||||
|
||||
const seriesValue = `${data.SeriesDescription} (#${data.SeriesNumber})`;
|
||||
root.push(getMainDataItem('Series', seriesValue));
|
||||
|
||||
const manufacturerValue = `${data.Manufacturer} (${
|
||||
data.ManufacturerModelName
|
||||
}, #${data.DeviceSerialNumber})`;
|
||||
|
||||
root.push(getMainDataItem('Manufacturer', manufacturerValue));
|
||||
|
||||
const mainDataObjects = {
|
||||
CompletionFlag: 'Completion flag',
|
||||
VerificationFlag: 'Verification flag'
|
||||
};
|
||||
|
||||
Object.keys(mainDataObjects).forEach(key => {
|
||||
if (!data[key]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = getMainDataItem(mainDataObjects[key], data[key]);
|
||||
|
||||
root.push(item);
|
||||
});
|
||||
|
||||
// TODO: Format these dates
|
||||
const contentDateTimeValue = `${data.ContentDate} ${data.ContentTime}`;
|
||||
root.push(getMainDataItem('Content Date/Time', contentDateTimeValue));
|
||||
|
||||
root.push();
|
||||
|
||||
return <div>{root}</div>;
|
||||
}
|
||||
|
||||
const getContentSequence = (data, level = 1) => {
|
||||
let header;
|
||||
|
||||
if (data.ConceptNameCodeSequence) {
|
||||
const {
|
||||
CodeMeaning,
|
||||
CodeValue,
|
||||
CodingSchemeDesignator
|
||||
} = data.ConceptNameCodeSequence;
|
||||
|
||||
header = `${CodeMeaning} (${CodeValue} - ${CodingSchemeDesignator})`;
|
||||
}
|
||||
|
||||
const root = [];
|
||||
if (header) {
|
||||
const HeaderDynamicLevel = `h${level}`;
|
||||
|
||||
root.push(<HeaderDynamicLevel key={header}>{header}</HeaderDynamicLevel>);
|
||||
}
|
||||
|
||||
Object.keys(data).forEach(key => {
|
||||
const value = data[key];
|
||||
|
||||
let content;
|
||||
if (value instanceof Object) {
|
||||
content = getContentSequence(value, level + 1);
|
||||
} else {
|
||||
content = (
|
||||
<div key={key}>
|
||||
{key} - {data[key]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
root.push(content);
|
||||
});
|
||||
|
||||
return <div>{root}</div>;
|
||||
};
|
||||
|
||||
function getMainDataItem(key, value) {
|
||||
return (
|
||||
<div key={key}>
|
||||
<b>{key}</b>: {value}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
class DicomHtmlViewport extends Component {
|
||||
state = {
|
||||
content: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
byteArray: TypedArrayProp.uint8
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const dataSet = this.setContentFromByteArray(this.props.byteArray);
|
||||
}
|
||||
|
||||
setContentFromByteArray(byteArray) {
|
||||
const arrayBuffer = byteArray.buffer;
|
||||
const dicomData = DicomMessage.readFile(arrayBuffer);
|
||||
const dataset = DicomMetaDictionary.naturalizeDataset(dicomData.dict);
|
||||
dataset._meta = DicomMetaDictionary.namifyDataset(dicomData.meta);
|
||||
|
||||
const mainData = getMainData(dataset);
|
||||
const contentSequence = getContentSequence(dataset);
|
||||
const content = (
|
||||
<>
|
||||
{mainData}
|
||||
{contentSequence}
|
||||
</>
|
||||
);
|
||||
|
||||
this.setState({
|
||||
content
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={'DicomHtmlViewport'}>
|
||||
{this.state.content}
|
||||
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DicomHtmlViewport;
|
||||
@@ -0,0 +1,27 @@
|
||||
import OHIFDicomHtmlViewport from './OHIFDicomHtmlViewport.js';
|
||||
import OHIFDicomHtmlSopClassHandler from './OHIFDicomHtmlSopClassHandler.js';
|
||||
|
||||
export default class OHIFDicomHtmlExtension {
|
||||
/**
|
||||
* Extension ID is a unique id, might be used for namespacing extension specific redux actions/reducers (?)
|
||||
*/
|
||||
getExtensionId() {
|
||||
return 'html';
|
||||
}
|
||||
|
||||
getViewportModule() {
|
||||
return OHIFDicomHtmlViewport;
|
||||
}
|
||||
|
||||
getSopClassHandler() {
|
||||
return OHIFDicomHtmlSopClassHandler;
|
||||
}
|
||||
|
||||
getPanelModuleDefinition() {
|
||||
return null;
|
||||
}
|
||||
|
||||
getToolbarModuleDefinition() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import OHIF from 'ohif-core';
|
||||
|
||||
const { plugins, utils } = OHIF;
|
||||
const { PLUGIN_TYPES } = plugins;
|
||||
|
||||
// TODO: Should probably use dcmjs for this
|
||||
const SOP_CLASS_UIDS = {
|
||||
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
|
||||
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
|
||||
COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33',
|
||||
PROCEDURE_LOG_STORAGE: '1.2.840.10008.5.1.4.1.1.88.40',
|
||||
MAMMOGRAPHY_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.50',
|
||||
CHEST_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.65',
|
||||
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67'
|
||||
};
|
||||
|
||||
const sopClassUids = Object.values(SOP_CLASS_UIDS);
|
||||
|
||||
// TODO: Handle the case where there is more than one SOP Class Handler for the
|
||||
// same SOP Class
|
||||
const OHIFDicomHtmlSopClassHandler = {
|
||||
id: 'OHIFDicomHtmlSopClassHandler',
|
||||
type: PLUGIN_TYPES.SOP_CLASS_HANDLER,
|
||||
sopClassUids,
|
||||
getDisplaySetFromSeries(series, study, dicomWebClient, authorizationHeaders) {
|
||||
const instance = series.getFirstInstance();
|
||||
|
||||
return {
|
||||
plugin: 'html',
|
||||
displaySetInstanceUid: utils.guid(),
|
||||
wadoRoot: study.getData().wadoRoot,
|
||||
wadoUri: instance.getData().wadouri,
|
||||
sopInstanceUid: instance.getSOPInstanceUID(),
|
||||
seriesInstanceUid: series.getSeriesInstanceUID(),
|
||||
studyInstanceUid: study.getStudyInstanceUID(),
|
||||
authorizationHeaders
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default OHIFDicomHtmlSopClassHandler;
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import DicomHtmlViewport from './DicomHtmlViewport';
|
||||
|
||||
class OHIFDicomHtmlViewport extends Component {
|
||||
static propTypes = {
|
||||
studies: PropTypes.object,
|
||||
displaySet: PropTypes.object,
|
||||
viewportIndex: PropTypes.number
|
||||
};
|
||||
|
||||
state = {
|
||||
byteArray: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { displaySet } = this.props.viewportData;
|
||||
const {
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid,
|
||||
wadoRoot,
|
||||
wadoUri,
|
||||
authorizationHeaders
|
||||
} = displaySet;
|
||||
|
||||
this.retrieveDicomData(
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid,
|
||||
wadoRoot,
|
||||
wadoUri,
|
||||
authorizationHeaders
|
||||
).then(
|
||||
byteArray => {
|
||||
this.setState({
|
||||
byteArray
|
||||
});
|
||||
},
|
||||
error => {
|
||||
this.setState({
|
||||
error
|
||||
});
|
||||
|
||||
throw new Error(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
retrieveDicomData(
|
||||
studyInstanceUid,
|
||||
seriesInstanceUid,
|
||||
sopInstanceUid,
|
||||
wadoRoot,
|
||||
wadoUri,
|
||||
authorizationHeaders
|
||||
) {
|
||||
// TODO: Passing in a lot of data we aren't using
|
||||
|
||||
// TODO: Authorization header depends on the server. If we ever have multiple servers
|
||||
// we will need to figure out how / when to pass this information in.
|
||||
return fetch(wadoUri, {
|
||||
headers: authorizationHeaders
|
||||
})
|
||||
.then(response => response.arrayBuffer())
|
||||
.then(arraybuffer => {
|
||||
return new Uint8Array(arraybuffer);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
{this.state.byteArray && (
|
||||
<DicomHtmlViewport byteArray={this.state.byteArray} />
|
||||
)}
|
||||
{this.state.error && <h2>{JSON.stringify(this.state.error)}</h2>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OHIFDicomHtmlViewport;
|
||||
@@ -0,0 +1,205 @@
|
||||
// https://github.com/facebook/prop-types/issues/69
|
||||
|
||||
export const TypedArrayProp = {
|
||||
any: (props, propName, componentName) => {
|
||||
let obj = props[propName];
|
||||
if (
|
||||
!(
|
||||
obj instanceof Float64Array ||
|
||||
obj instanceof Float32Array ||
|
||||
obj instanceof Int32Array ||
|
||||
obj instanceof Int16Array ||
|
||||
obj instanceof Int8Array ||
|
||||
obj instanceof Uint32Array ||
|
||||
obj instanceof Uint16Array ||
|
||||
obj instanceof Uint8Array ||
|
||||
obj instanceof Uint8ClampedArray
|
||||
)
|
||||
) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected a typed array.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
float64: (props, propName, componentName) => {
|
||||
if (!(props[propName] instanceof Float64Array)) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected a Float64Array.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
float32: (props, propName, componentName) => {
|
||||
if (!(props[propName] instanceof Float32Array)) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected a Float32Array.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
float: (props, propName, componentName) => {
|
||||
if (
|
||||
!(
|
||||
props[propName] instanceof Float64Array ||
|
||||
props[propName] instanceof Float32Array
|
||||
)
|
||||
) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected a Float32Array or Float64Array.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
int32: (props, propName, componentName) => {
|
||||
if (!(props[propName] instanceof Int32Array)) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected an Int32Array.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
int16: (props, propName, componentName) => {
|
||||
if (!(props[propName] instanceof Int16Array)) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected an In16Array.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
int8: (props, propName, componentName) => {
|
||||
if (!(props[propName] instanceof Int8Array)) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected an Int8Array.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
int: (props, propName, componentName) => {
|
||||
if (
|
||||
!(
|
||||
props[propName] instanceof Int32Array ||
|
||||
props[propName] instanceof Int16Array ||
|
||||
props[propName] instanceof Int8Array
|
||||
)
|
||||
) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected an Int32Array, In16Array, or Int8Array.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
uint32: (props, propName, componentName) => {
|
||||
if (!(props[propName] instanceof Uint32Array)) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected a Uint32Array.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
uint16: (props, propName, componentName) => {
|
||||
if (!(props[propName] instanceof Uint16Array)) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected a Uint16Array.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
uint8: (props, propName, componentName) => {
|
||||
if (!(props[propName] instanceof Uint8Array)) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected a Uint8Array.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
uint8clamped: (props, propName, componentName) => {
|
||||
if (!(props[propName] instanceof Uint8ClampedArray)) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected a Uint8ClampedArray.'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
uint: (props, propName, componentName) => {
|
||||
if (
|
||||
!(
|
||||
props[propName] instanceof Uint32Array ||
|
||||
props[propName] instanceof Uint16Array ||
|
||||
props[propName] instanceof Uint8Array ||
|
||||
props[propName] instanceof Uint8ClampedArray
|
||||
)
|
||||
) {
|
||||
return new Error(
|
||||
'Invalid prop `' +
|
||||
propName +
|
||||
'` supplied to' +
|
||||
' `' +
|
||||
componentName +
|
||||
'`. Expected a Uint32Array, Uint16Array, Uint8Array, or Uint8ClampedArray.'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { TypedArrayProp as default };
|
||||
@@ -0,0 +1,3 @@
|
||||
import OHIFDicomHtmlExtension from './OHIFDicomHtmlExtension.js';
|
||||
|
||||
export default OHIFDicomHtmlExtension;
|
||||
Reference in new issue
Block a user