Compare commits

...
Author SHA1 Message Date
ohif-bot 36910f2b47 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.3
 - @ohif/core@1.1.0
 - @ohif/ui@0.53.0
 - @ohif/viewer@1.4.0
2019-10-03 08:02:00 +00:00
Emanuel Fiuza de Oliveira 9e10c2b2de feat: Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once (#953) 2019-10-03 09:51:29 +02:00
ohif-bot c5cbd77cb9 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.2
 - @ohif/core@1.0.2
 - @ohif/viewer@1.3.3
2019-10-02 07:49:25 +00:00
Erik Ziegler b3b4c10e4c fix: Temporarily sort SEG files to the end of the display set list as a workaround for several metadata issues (#987) 2019-10-02 09:39:38 +02:00
ohif-bot 855020d730 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.1
 - @ohif/viewer@1.3.2
2019-10-01 20:36:02 +00:00
Zaid Safadi 8503cbce4e fix(rotate): Use new MPR rotate in react-vtkjs-viewport 0.1.2 (#964) 2019-10-01 22:21:31 +02:00
ohif-bot c8667fb2d6 chore(release): publish [skip ci]
- @ohif/viewer@1.3.1
2019-10-01 10:07:08 +00:00
Erik Ziegler 674ca9f96f fix: Exit MPR mode if Layout is changed (#984) 2019-10-01 11:55:58 +02:00
ohif-bot 7a4b97b4e8 chore(release): publish [skip ci]
- @ohif/extension-vtk@0.51.0
 - @ohif/ui@0.52.0
 - @ohif/viewer@1.3.0
2019-10-01 08:16:51 +00:00
ladeirarodolfo c79c0c301d feat: 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle (#947) 2019-10-01 10:06:56 +02:00
ohif-bot ff5060f276 chore(release): publish [skip ci]
- @ohif/viewer@1.2.8
2019-10-01 07:42:58 +00:00
Erik Ziegler dafb26b34b chore: Fix dev server so that it can be accessed on LAN from other devices (#983) 2019-10-01 09:32:50 +02:00
36 changed files with 1261 additions and 458 deletions

No files matched your search

+38
View File
@@ -3,6 +3,44 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.51.3](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.2...@ohif/extension-vtk@0.51.3) (2019-10-03)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.51.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.1...@ohif/extension-vtk@0.51.2) (2019-10-02)
**Note:** Version bump only for package @ohif/extension-vtk
## [0.51.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.51.0...@ohif/extension-vtk@0.51.1) (2019-10-01)
### Bug Fixes
* **rotate:** Use new MPR rotate in react-vtkjs-viewport 0.1.2 ([#964](https://github.com/OHIF/Viewers/issues/964)) ([8503cbc](https://github.com/OHIF/Viewers/commit/8503cbc))
# [0.51.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.23...@ohif/extension-vtk@0.51.0) (2019-10-01)
### Features
* 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle ([#947](https://github.com/OHIF/Viewers/issues/947)) ([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3))
## [0.50.23](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@0.50.22...@ohif/extension-vtk@0.50.23) (2019-10-01)
**Note:** Version bump only for package @ohif/extension-vtk
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/extension-vtk",
"version": "0.50.23",
"version": "0.51.3",
"description": "OHIF extension for VTK.js",
"author": "OHIF",
"license": "MIT",
@@ -48,11 +48,11 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"react-vtkjs-viewport": "0.1.0"
"react-vtkjs-viewport": "0.1.2"
},
"devDependencies": {
"@ohif/core": "^1.0.1",
"@ohif/ui": "^0.51.3",
"@ohif/core": "^1.1.0",
"@ohif/ui": "^0.53.0",
"cornerstone-tools": "^4.0.9",
"cornerstone-wado-image-loader": "^3.0.0",
"dcmjs": "^0.6.1",
+44 -24
View File
@@ -2,6 +2,7 @@ import {
vtkInteractorStyleMPRCrosshairs,
vtkInteractorStyleMPRWindowLevel,
vtkInteractorStyleMPRSlice,
vtkInteractorStyleMPRRotate,
vtkSVGCrosshairsWidget,
vtkSVGWidgetManager,
} from 'react-vtkjs-viewport';
@@ -18,6 +19,7 @@ const { BlendMode } = Constants;
// TODO: Put this somewhere else
let apis = {};
let currentSlabThickness = 0.1;
let defaultSlabThickness = 0.1;
function getCrosshairCallbackForIndex(index) {
return ({ worldPos }) => {
@@ -98,7 +100,7 @@ function _setView(api, sliceNormal, viewUp) {
const camera = renderer.getActiveCamera();
const istyle = renderWindow.getInteractor().getInteractorStyle();
istyle.setSliceNormal(...sliceNormal);
camera.setViewUp(...viewUp);
istyle.setViewUp(...viewUp);
renderWindow.render();
}
@@ -109,14 +111,15 @@ function switchMPRInteractors(api, istyle) {
const camera = renderer.getActiveCamera();
const currentIStyle = renderWindow.getInteractor().getInteractorStyle();
let currentNormal;
if (currentIStyle.getSliceNormal && istyle.getSliceNormal) {
currentNormal = currentIStyle.getSliceNormal();
let currentViewport;
if (currentIStyle.getViewport && istyle.getViewport) {
currentViewport = currentIStyle.getViewport();
}
let currentSlabThickness;
if (currentIStyle.getSlabThickness && istyle.getSlabThickness) {
currentSlabThickness = currentIStyle.getSlabThickness();
defaultSlabThickness = currentSlabThickness;
}
renderWindow.getInteractor().setInteractorStyle(istyle);
@@ -124,11 +127,11 @@ function switchMPRInteractors(api, istyle) {
// TODO: Not sure why this is required the second time this function is called
istyle.setInteractor(renderWindow.getInteractor());
if (istyle.getVolumeMapper() !== api.volumes[0]) {
if (currentNormal) {
istyle.setSliceNormal(currentNormal);
}
if (currentViewport) {
istyle.setViewport(currentViewport);
}
if (istyle.getVolumeMapper() !== api.volumes[0]) {
if (currentSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
}
@@ -161,7 +164,7 @@ const actions = {
},
enableRotateTool: () => {
apis.forEach(api => {
const istyle = vtkInteractorStyleMPRSlice.newInstance();
const istyle = vtkInteractorStyleMPRRotate.newInstance();
switchMPRInteractors(api, istyle);
});
@@ -182,7 +185,7 @@ const actions = {
switchMPRInteractors(api, istyle);
});
},
setSlabThickness: slabThickness => {
setSlabThickness: ({ slabThickness }) => {
currentSlabThickness = slabThickness;
apis.forEach(api => {
@@ -191,15 +194,6 @@ const actions = {
if (istyle.setSlabThickness) {
istyle.setSlabThickness(currentSlabThickness);
// TODO: Do this inside the interactors in a setSlabThickness function instead
const renderer = api.genericRenderWindow.getRenderer();
const camera = renderer.getActiveCamera();
const dist = camera.getDistance();
const near = dist - currentSlabThickness / 2;
const far = dist + currentSlabThickness / 2;
camera.setClippingRange(near, far);
}
renderWindow.render();
@@ -220,6 +214,32 @@ const actions = {
renderWindow.render();
});
},
setBlendModeToComposite: () => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const istyle = renderWindow.getInteractor().getInteractorStyle();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToComposite) {
mapper.setBlendModeToComposite();
}
if (istyle.setSlabThickness) {
istyle.setSlabThickness(defaultSlabThickness);
}
renderWindow.render();
});
},
setBlendModeToMaximumIntensity: () => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToMaximumIntensity) {
mapper.setBlendModeToMaximumIntensity();
}
renderWindow.render();
});
},
setBlendMode: ({ blendMode }) => {
apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();
@@ -284,18 +304,18 @@ const actions = {
case 0:
//Axial
istyle.setSliceNormal(0, 0, 1);
camera.setViewUp(0, -1, 0);
istyle.setViewUp(0, -1, 0);
break;
case 1:
// sagittal
istyle.setSliceNormal(1, 0, 0);
camera.setViewUp(0, 0, 1);
istyle.setViewUp(0, 0, 1);
break;
case 2:
// Coronal
istyle.setSliceNormal(0, 1, 0);
camera.setViewUp(0, 0, 1);
istyle.setViewUp(0, 0, 1);
break;
}
@@ -338,12 +358,12 @@ const definitions = {
options: {},
},
setBlendModeToComposite: {
commandFn: actions.setBlendMode,
commandFn: actions.setBlendModeToComposite,
storeContexts: [],
options: { blendMode: BlendMode.COMPOSITE_BLEND },
},
setBlendModeToMaximumIntensity: {
commandFn: actions.setBlendMode,
commandFn: actions.setBlendModeToMaximumIntensity,
storeContexts: [],
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
},
@@ -0,0 +1,207 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Range, Checkbox, Select } from '@ohif/ui';
import './slab-thickness-toolbar-button.styl';
const SLIDER = {
MIN: 0.1,
MAX: 1000,
STEP: 0.1,
};
const ToolbarLabel = props => {
const { label } = props;
return <div className="toolbar-button-label">{label}</div>;
};
const ToolbarSlider = props => {
const { value, min, max, onChange } = props;
return (
<div className="toolbar-slider-container">
<label htmlFor="toolbar-slider">{value}mm</label>
<Range
value={value}
min={min}
max={max}
step={SLIDER.STEP}
onChange={onChange}
id="toolbar-slider"
/>
</div>
);
};
const _getSelectOptions = button => {
return button.operationButtons.map(button => {
return {
key: button.label,
value: button.id,
};
});
};
const _getClassNames = (isActive, className) => {
return classnames('toolbar-button', 'slab-thickness', className, {
active: isActive,
});
};
const _applySlabThickness = (
value,
modeChecked,
toolbarClickCallback,
button
) => {
if (!modeChecked || !toolbarClickCallback) {
return;
}
const { actionButton } = button;
const generateOperation = (operation, value) => {
// Combine slider value into slider operation
const generatedOperation = { ...operation };
generatedOperation.commandOptions = {
...operation.commandOptions,
slabThickness: value,
};
return generatedOperation;
};
const operation = generateOperation(actionButton, value);
toolbarClickCallback(operation, event);
};
const _applyModeOperation = (
operation,
modeChecked,
toolbarClickCallback,
button
) => {
// in case modeChecked has not being triggered by user yet
if (typeof modeChecked !== 'boolean') {
return;
}
const { deactivateButton } = button;
const _operation = modeChecked ? operation : deactivateButton;
if (toolbarClickCallback && _operation) {
toolbarClickCallback(_operation);
}
};
const _getInitialState = currentSelectedOption => {
return {
value: SLIDER.MIN,
sliderMin: SLIDER.MIN,
sliderMax: SLIDER.MAX,
modeChecked: undefined,
operation: currentSelectedOption,
};
};
const INITIAL_OPTION_INDEX = 0;
const _getInitialtSelectedOption = (button = {}) => {
return (
button.operationButtons && button.operationButtons[INITIAL_OPTION_INDEX]
);
};
function SlabThicknessToolbarComponent({
parentContext,
toolbarClickCallback,
button,
activeButtons,
isActive,
className,
}) {
const currentSelectedOption = _getInitialtSelectedOption(button);
const [state, setState] = useState(_getInitialState(currentSelectedOption));
const { label, operationButtons } = button;
const _className = _getClassNames(isActive, className);
const selectOptions = _getSelectOptions(button);
function onChangeSelect(selectedValue) {
// find select value
const operation = operationButtons.find(
button => button.id === selectedValue
);
if (operation === state.operation) {
return;
}
setState({ ...state, operation });
}
function onChangeCheckbox(checked) {
setState({ ...state, modeChecked: checked });
}
function onChangeSlider(event) {
const value = Number(event.target.value);
if (value !== state.value) {
setState({ ...state, value, modeChecked: true });
}
}
useEffect(() => {
_applyModeOperation(
state.operation,
state.modeChecked,
toolbarClickCallback,
button
);
}, [state.modeChecked, state.operation]);
useEffect(() => {
_applySlabThickness(
state.value,
state.modeChecked,
toolbarClickCallback,
button
);
}, [state.operation, state.modeChecked, state.value]);
return (
<div className={_className}>
<div className="container">
<ToolbarSlider
value={state.value}
min={state.sliderMin}
max={state.sliderMax}
onChange={onChangeSlider}
/>
<ToolbarLabel key="toolbar-label" label={label} />
</div>
<div className="controller">
<Checkbox
label="mode"
checked={state.modeChecked}
onChange={onChangeCheckbox}
></Checkbox>
<Select
key="toolbar-select"
options={selectOptions}
value={selectOptions[INITIAL_OPTION_INDEX].value}
onChange={onChangeSelect}
></Select>
</div>
</div>
);
}
SlabThicknessToolbarComponent.propTypes = {
parentContext: PropTypes.object.isRequired,
toolbarClickCallback: PropTypes.func.isRequired,
button: PropTypes.object.isRequired,
activeButtons: PropTypes.array.isRequired,
isActive: PropTypes.bool,
className: PropTypes.string,
};
export default SlabThicknessToolbarComponent;
@@ -0,0 +1,55 @@
.slab-thickness
display: flex;
flex-direction: row;
align-items: flex-end;
height: 60px;
margin-top: -12px;
&:hover
color: var(--default-color);
.container
padding-right: 10px;
height: inherit;
display: flex;
flex-direction: column;
justify-content: space-between;
.toolbar-slider-container
width: 80px;
margin: 0px 2px 2px 2px;
font-size: 12px;
&:hover
color: var(--hover-color);
input
height: 20px;
padding-top: 6px;
&::-moz-range-thumb
height: 16px;
margin-top: -7px;
&::-webkit-slider-thumb
height: 16px;
margin-top: -7px;
.controller
width: 62px;
.ohif-check-container
margin-bottom: 5px;
padding-left: 18px;
.ohif-check-label
text-transform: capitalize;
&:hover
color: var(--hover-color);
.ohif-checkbox
border-radius: 3px;
background-color: white;
width: 12px;
height: 12px;
top: 22%;
&.ohif-checked:after
background-color: var(--active-color);
top: 17%;
left: 17%;
.select-ohif
color: var(--default-color);
border-color: var(--default-color);
background-color: black;
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E');
&:hover
color: var(--hover-color);
+47
View File
@@ -1,3 +1,5 @@
import SlabThicknessToolbarComponent from './toolbarComponents/SlabThicknessToolbarComponent';
const TOOLBAR_BUTTON_TYPES = {
COMMAND: 'command',
SET_TOOL_ACTIVE: 'setToolActive',
@@ -31,6 +33,7 @@ const definitions = [
commandName: 'enableRotateTool',
commandOptions: {},
},
/*
{
id: 'setBlendModeToComposite',
label: 'Disable MIP',
@@ -68,6 +71,50 @@ const definitions = [
commandName: 'decreaseSlabThickness',
commandOptions: {},
},
*/
{
id: 'changeSlabThickness',
label: 'Slab Thickness',
icon: 'soft-tissue',
CustomComponent: SlabThicknessToolbarComponent,
commandName: 'setSlabThickness',
actionButton: {
id: 'setSlabThickness',
label: 'slider',
type: TOOLBAR_BUTTON_TYPES.COMMAND,
commandName: 'setSlabThickness',
commandOptions: {},
},
deactivateButton: {
id: 'setBlendModeToComposite',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToComposite',
commandOptions: {},
},
operationButtons: [
{
id: 'setBlendModeToMaximumIntensity',
label: 'MIP',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToMaximumIntensity',
commandOptions: {},
},
{
id: 'setBlendModeToMinimumIntensity',
label: 'MinIP',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToMinimumIntensity',
commandOptions: {},
},
{
id: 'setBlendModeToAverageIntensity',
label: 'AvgIP',
type: TOOLBAR_BUTTON_TYPES.SET_TOOL_ACTIVE,
commandName: 'setBlendModeToAverageIntensity',
commandOptions: {},
},
],
},
];
export default {
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.1.0](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.2...@ohif/core@1.1.0) (2019-10-03)
### Features
* Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953)) ([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b))
## [1.0.2](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.1...@ohif/core@1.0.2) (2019-10-02)
### Bug Fixes
* Temporarily sort SEG files to the end of the display set list as a workaround for several metadata issues ([#987](https://github.com/OHIF/Viewers/issues/987)) ([b3b4c10](https://github.com/OHIF/Viewers/commit/b3b4c10))
## [1.0.1](https://github.com/OHIF/Viewers/compare/@ohif/core@1.0.0...@ohif/core@1.0.1) (2019-09-27)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/core",
"version": "1.0.1",
"version": "1.1.0",
"description": "Generic business logic for web-based medical imaging applications",
"author": "OHIF Core Team",
"license": "MIT",
@@ -1,10 +1,12 @@
// import { api } from 'dicomweb-client'
const api = {
DICOMwebClient: jest.fn().mockImplementation(() => {
return {
retrieveStudyMetadata: jest.fn().mockResolvedValue([]),
};
DICOMwebClient: jest.fn().mockImplementation(function() {
this.retrieveStudyMetadata = jest.fn().mockResolvedValue([]);
this.retrieveSeriesMetadata = jest.fn(function(options) {
const { studyInstanceUID, seriesInstanceUID } = options;
return Promise.resolve([{ studyInstanceUID, seriesInstanceUID }]);
});
}),
};
@@ -8,7 +8,8 @@ import { SeriesMetadata } from './SeriesMetadata';
// - createStacks
import { api } from 'dicomweb-client';
// - createStacks
import { isImage } from './../../utils/isImage';
import { isImage } from '../../utils/isImage';
import isLowPriorityModality from '../../utils/isLowPriorityModality';
export class StudyMetadata extends Metadata {
constructor(data, uid) {
@@ -88,6 +89,100 @@ export class StudyMetadata extends Metadata {
return this._displaySets.slice();
}
/**
* Split a series metadata object into display sets
* @param {Array} sopClassHandlerModules List of SOP Class Modules
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
* @param {Array} [givenDisplaySets] An optional list to which the display sets will be appended
* @returns {Array} The list of display sets created for the given series object
*/
_createDisplaySetsForSeries(
sopClassHandlerModules,
series,
givenDisplaySets
) {
const study = this;
const displaySets = Array.isArray(givenDisplaySets) ? givenDisplaySets : [];
const anyInstances = series.getInstanceCount() > 0;
if (!anyInstances) {
return;
}
const sopClassUids = getSopClassUids(series);
if (sopClassHandlerModules && sopClassHandlerModules.length > 0) {
const displaySet = _getDisplaySetFromSopClassModule(
sopClassHandlerModules,
series,
study,
sopClassUids
);
if (displaySet) {
displaySet.sopClassModule = true;
displaySets.push(displaySet);
return;
}
}
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
// Search through the instances (InstanceMetadata object) of this series
// Split Multi-frame instances and Single-image modalities
// into their own specific display sets. Place the rest of each
// series into another display set.
const stackableInstances = [];
series.forEachInstance(instance => {
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
if (
!isImage(instance.getRawValue('x00080016')) &&
!instance.getRawValue('x00280010')
) {
return;
}
let displaySet;
if (isMultiFrame(instance)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
isClip: true,
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else if (isSingleImageModality(instance.modality)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
seriesInstanceUid: series.getSeriesInstanceUID(),
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else {
stackableInstances.push(instance);
}
});
if (stackableInstances.length) {
const displaySet = makeDisplaySet(series, stackableInstances);
displaySet.setAttribute('studyInstanceUid', study.getStudyInstanceUID());
displaySet.setAttributes({
sopClassUids,
});
displaySets.push(displaySet);
}
return displaySets;
}
/**
* Creates a set of series to be placed in the Study Metadata
* The series that appear in the Study Metadata must represent
@@ -101,104 +196,44 @@ export class StudyMetadata extends Metadata {
* @returns {Array} An array of series to be placed in the Study Metadata
*/
createDisplaySets(sopClassHandlerModules) {
const study = this;
const displaySets = [];
const anyDisplaySets = study.getSeriesCount();
const anySopClassHandlerModules =
sopClassHandlerModules && sopClassHandlerModules.length > 0;
const anyDisplaySets = this.getSeriesCount();
if (!anyDisplaySets) {
return displaySets;
}
// Loop through the series (SeriesMetadata)
this.forEachSeries(series => {
const anyInstances = series.getInstanceCount() > 0;
if (!anyInstances) {
return;
}
const sopClassUids = getSopClassUids(series);
if (anySopClassHandlerModules) {
const displaySet = _getDisplaySetFromSopClassModule(
this.forEachSeries(
series =>
void this._createDisplaySetsForSeries(
sopClassHandlerModules,
series,
study,
sopClassUids
);
displaySets
)
);
if (displaySet) {
displaySet.sopClassModule = true;
displaySets.push(displaySet);
return sortDisplaySetList(displaySets);
}
return;
}
}
sortDisplaySets() {
sortDisplaySetList(this._displaySets);
}
// WE NEED A BETTER WAY TO NOTE THAT THIS IS THE DEFAULT BEHAVIOR FOR LOADING
// A DISPLAY SET IF THERE IS NO MATCHING SOP CLASS PLUGIN
// Search through the instances (InstanceMetadata object) of this series
// Split Multi-frame instances and Single-image modalities
// into their own specific display sets. Place the rest of each
// series into another display set.
const stackableInstances = [];
series.forEachInstance(instance => {
// All imaging modalities must have a valid value for sopClassUid (x00080016) or rows (x00280010)
if (
!isImage(instance.getRawValue('x00080016')) &&
!instance.getRawValue('x00280010')
) {
return;
}
let displaySet;
if (isMultiFrame(instance)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
isClip: true,
seriesInstanceUid: series.getSeriesInstanceUID(),
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid for drag/drop purposes
numImageFrames: instance.getRawValue('x00280008'), // Override the default value of instances.length
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else if (isSingleImageModality(instance.modality)) {
displaySet = makeDisplaySet(series, [instance]);
displaySet.setAttributes({
sopClassUids,
studyInstanceUid: study.getStudyInstanceUID(), // Include the study instance Uid
seriesInstanceUid: series.getSeriesInstanceUID(),
instanceNumber: instance.getRawValue('x00200013'), // Include the instance number
acquisitionDatetime: instance.getRawValue('x0008002a'), // Include the acquisition datetime
});
displaySets.push(displaySet);
} else {
stackableInstances.push(instance);
}
});
if (stackableInstances.length) {
const displaySet = makeDisplaySet(series, stackableInstances);
displaySet.setAttribute(
'studyInstanceUid',
study.getStudyInstanceUID()
);
displaySet.setAttributes({
sopClassUids,
});
displaySets.push(displaySet);
}
});
// TODO
displaySets.sort(_sortBySeriesNumber);
return displaySets;
/**
* Method to append display sets from a given series to the internal list of display sets
* @param {Array} sopClassHandlerModules A list of SOP Class Handler Modules
* @param {SeriesMetadata} series The series metadata object from which the display sets will be created
* @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
*/
createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
if (this.containsSeries(series)) {
this.setDisplaySets(
this._createDisplaySetsForSeries(sopClassHandlerModules, series)
);
return true;
}
return false;
}
/**
@@ -206,7 +241,10 @@ export class StudyMetadata extends Metadata {
* @param {Array} displaySets Array of display sets (ImageSet[])
*/
setDisplaySets(displaySets) {
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
if (Array.isArray(displaySets) && displaySets.length > 0) {
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
this.sortDisplaySets();
}
}
/**
@@ -320,6 +358,12 @@ export class StudyMetadata extends Metadata {
return found;
}
containsSeries(series) {
return (
series instanceof SeriesMetadata && this._series.indexOf(series) >= 0
);
}
/**
* Retrieve the number of series within the current study.
* @returns {number} The number of series in the current study.
@@ -650,13 +694,51 @@ function _getDisplaySetFromSopClassModule(
}
/**
* Sort series primarily by modality (i.e., series with references to other
* series like SEG, KO or PR are grouped in the end of the list) and then by
* series number:
*
* --------
* | CT #3 |
* | CT #4 |
* | CT #5 |
* --------
* | SEG #1 |
* | SEG #2 |
* --------
*
* @param {*} a - DisplaySet
* @param {*} b - DisplaySet
*/
function _sortBySeriesNumber(a, b) {
function seriesSortingCriteria(a, b) {
const isLowPriorityA = isLowPriorityModality(a.modality);
const isLowPriorityB = isLowPriorityModality(b.modality);
if (!isLowPriorityA && isLowPriorityB) {
return -1;
}
if (isLowPriorityA && !isLowPriorityB) {
return 1;
}
return sortBySeriesNumber(a, b);
}
/**
* Sort series by series number. Series with low
* @param {*} a - DisplaySet
* @param {*} b - DisplaySet
*/
function sortBySeriesNumber(a, b) {
const seriesNumberAIsGreaterOrUndefined =
a.seriesNumber > b.seriesNumber || (!a.seriesNumber && b.seriesNumber);
return seriesNumberAIsGreaterOrUndefined ? 1 : -1;
}
/**
* Sorts a list of display set objects
* @param {Array} list A list of display sets to be sorted
*/
function sortDisplaySetList(list) {
return list.sort(seriesSortingCriteria);
}
@@ -1,5 +1,8 @@
import { api } from 'dicomweb-client';
import DICOMWeb from '../../../DICOMWeb/';
import isLowPriorityModality from '../../../utils/isLowPriorityModality';
const INFO = Symbol('INFO');
const WADOProxy = {
convertURL: (url, server) => {
@@ -94,9 +97,7 @@ function buildInstanceWadoRsUri(
seriesInstanceUid,
sopInstanceUid
) {
return `${
server.wadoRoot
}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
return `${server.wadoRoot}/studies/${studyInstanceUid}/series/${seriesInstanceUid}/instances/${sopInstanceUid}`;
}
function buildInstanceFrameWadoRsUri(
@@ -166,9 +167,9 @@ function getPaletteColor(server, instance, tag, lutDescriptor) {
};
const arrayBufferToPaletteColorLUT = result => {
const arraybuffer = result[0]
const byteArray = new Uint8Array(arraybuffer)
const lut = []
const arraybuffer = result[0];
const byteArray = new Uint8Array(arraybuffer);
const lut = [];
for (let i = 0; i < numLutEntries; i++) {
if (bits === 16) {
@@ -271,200 +272,338 @@ function getRadiopharmaceuticalInfo(instance) {
};
}
async function makeSOPInstance(server, study, instance) {
const { studyInstanceUid } = study;
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
let series = study.seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesInstanceUid,
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality: DICOMWeb.getString(instance['00080060']),
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
seriesDate: DICOMWeb.getString(instance['00080021']),
seriesTime: DICOMWeb.getString(instance['00080031']),
instances: [],
};
study.seriesMap[seriesInstanceUid] = series;
study.seriesList.push(series);
}
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
const wadouri = buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const wadorsuri = buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const sopInstance = {
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
modality: DICOMWeb.getString(instance['00080060']),
sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(DICOMWeb.getString(instance['00181065'])),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
baseWadoRsUri: baseWadoRsUri,
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
wadoRoot: server.wadoRoot,
imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering,
};
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
if (sopInstance.photometricInterpretation === 'PALETTE COLOR') {
const redPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281101'])
);
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281102'])
);
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281103'])
);
const palettes = await getPaletteColors(
server,
instance,
redPaletteColorLookupTableDescriptor
);
if (palettes) {
if (palettes.uid) {
sopInstance.paletteColorLookupTableUID = palettes.uid;
}
sopInstance.redPaletteColorLookupTableData = palettes.red;
sopInstance.greenPaletteColorLookupTableData = palettes.green;
sopInstance.bluePaletteColorLookupTableData = palettes.blue;
sopInstance.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
sopInstance.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
sopInstance.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
}
}
series.instances.push(sopInstance);
return sopInstance;
}
/**
* Create a plain JS object that describes a study (a study descriptor object)
* @param {Object} server Object with server configuration paramenters
* @param {Object} aSopInstance a SOP Instance from which study information will be added
*/
function createStudy(server, aSopInstance) {
// TODO: Pass a reference ID to the server instead of including the URLs here
return {
seriesList: [],
seriesMap: Object.create(null),
seriesLoader: null,
wadoUriRoot: server.wadoUriRoot,
wadoRoot: server.wadoRoot,
qidoRoot: server.qidoRoot,
patientName: DICOMWeb.getName(aSopInstance['00100010']),
patientId: DICOMWeb.getString(aSopInstance['00100020']),
patientAge: DICOMWeb.getNumber(aSopInstance['00101010']),
patientSize: DICOMWeb.getNumber(aSopInstance['00101020']),
patientWeight: DICOMWeb.getNumber(aSopInstance['00101030']),
accessionNumber: DICOMWeb.getString(aSopInstance['00080050']),
studyDate: DICOMWeb.getString(aSopInstance['00080020']),
modalities: DICOMWeb.getString(aSopInstance['00080061']),
studyDescription: DICOMWeb.getString(aSopInstance['00081030']),
imageCount: DICOMWeb.getString(aSopInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(aSopInstance['0020000D']),
institutionName: DICOMWeb.getString(aSopInstance['00080080']),
};
}
/**
* Add a list of SOP Instances to a given study object descriptor
* @param {Object} server Object with server configuration paramenters
* @param {Object} study The study descriptor to which the given SOP instances will be added
* @param {Array} sopInstanceList A list of SOP instance objects
*/
async function addInstancesToStudy(server, study, sopInstanceList) {
return Promise.all(
sopInstanceList.map(function(sopInstance) {
return makeSOPInstance(server, study, sopInstance);
})
);
}
/**
* Parses result data from a WADO search into Study MetaData
* Returns an object populated with study metadata, including the
* series list.
*
* @param server
* @param studyInstanceUid
* @param resultData
* @returns {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
* @param {Object} server Object with server configuration paramenters
* @param {Array} sopInstanceList List of SOP Instances that build up to the study
* @resolves {{seriesList: Array, patientName: *, patientId: *, accessionNumber: *, studyDate: *, modalities: *, studyDescription: *, imageCount: *, studyInstanceUid: *}}
*/
async function resultDataToStudyMetadata(server, studyInstanceUid, resultData) {
if (!resultData.length) {
return;
async function createStudyFromSOPInstanceList(server, sopInstanceList) {
if (Array.isArray(sopInstanceList) && sopInstanceList.length > 0) {
const firstSopInstance = sopInstanceList[0];
const study = createStudy(server, firstSopInstance);
await addInstancesToStudy(server, study, sopInstanceList);
return study;
}
const anInstance = resultData[0];
if (!anInstance) {
return;
}
// TODO: Pass a reference ID to the server instead of including the URLs here
const studyData = {
seriesList: [],
wadoUriRoot: server.wadoUriRoot,
wadoRoot: server.wadoRoot,
qidoRoot: server.qidoRoot,
patientName: DICOMWeb.getName(anInstance['00100010']),
patientId: DICOMWeb.getString(anInstance['00100020']),
patientAge: DICOMWeb.getNumber(anInstance['00101010']),
patientSize: DICOMWeb.getNumber(anInstance['00101020']),
patientWeight: DICOMWeb.getNumber(anInstance['00101030']),
accessionNumber: DICOMWeb.getString(anInstance['00080050']),
studyDate: DICOMWeb.getString(anInstance['00080020']),
modalities: DICOMWeb.getString(anInstance['00080061']),
studyDescription: DICOMWeb.getString(anInstance['00081030']),
imageCount: DICOMWeb.getString(anInstance['00201208']),
studyInstanceUid: DICOMWeb.getString(anInstance['0020000D']),
institutionName: DICOMWeb.getString(anInstance['00080080']),
};
const seriesMap = {};
await Promise.all(
resultData.map(async function(instance) {
const seriesInstanceUid = DICOMWeb.getString(instance['0020000E']);
let series = seriesMap[seriesInstanceUid];
if (!series) {
series = {
seriesDescription: DICOMWeb.getString(instance['0008103E']),
modality: DICOMWeb.getString(instance['00080060']),
seriesInstanceUid: seriesInstanceUid,
seriesNumber: DICOMWeb.getNumber(instance['00200011']),
seriesDate: DICOMWeb.getString(instance['00080021']),
seriesTime: DICOMWeb.getString(instance['00080031']),
instances: [],
};
seriesMap[seriesInstanceUid] = series;
studyData.seriesList.push(series);
}
const sopInstanceUid = DICOMWeb.getString(instance['00080018']);
const wadouri = buildInstanceWadoUrl(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const baseWadoRsUri = buildInstanceWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const wadorsuri = buildInstanceFrameWadoRsUri(
server,
studyInstanceUid,
seriesInstanceUid,
sopInstanceUid
);
const instanceSummary = {
imageType: DICOMWeb.getString(instance['00080008']),
sopClassUid: DICOMWeb.getString(instance['00080016']),
modality: DICOMWeb.getString(instance['00080060']),
sopInstanceUid,
instanceNumber: DICOMWeb.getNumber(instance['00200013']),
imagePositionPatient: DICOMWeb.getString(instance['00200032']),
imageOrientationPatient: DICOMWeb.getString(instance['00200037']),
frameOfReferenceUID: DICOMWeb.getString(instance['00200052']),
sliceLocation: DICOMWeb.getNumber(instance['00201041']),
samplesPerPixel: DICOMWeb.getNumber(instance['00280002']),
photometricInterpretation: DICOMWeb.getString(instance['00280004']),
planarConfiguration: DICOMWeb.getNumber(instance['00280006']),
rows: DICOMWeb.getNumber(instance['00280010']),
columns: DICOMWeb.getNumber(instance['00280011']),
pixelSpacing: DICOMWeb.getString(instance['00280030']),
pixelAspectRatio: DICOMWeb.getString(instance['00280034']),
bitsAllocated: DICOMWeb.getNumber(instance['00280100']),
bitsStored: DICOMWeb.getNumber(instance['00280101']),
highBit: DICOMWeb.getNumber(instance['00280102']),
pixelRepresentation: DICOMWeb.getNumber(instance['00280103']),
smallestPixelValue: DICOMWeb.getNumber(instance['00280106']),
largestPixelValue: DICOMWeb.getNumber(instance['00280107']),
windowCenter: DICOMWeb.getString(instance['00281050']),
windowWidth: DICOMWeb.getString(instance['00281051']),
rescaleIntercept: DICOMWeb.getNumber(instance['00281052']),
rescaleSlope: DICOMWeb.getNumber(instance['00281053']),
rescaleType: DICOMWeb.getNumber(instance['00281054']),
sourceImageInstanceUid: getSourceImageInstanceUid(instance),
laterality: DICOMWeb.getString(instance['00200062']),
viewPosition: DICOMWeb.getString(instance['00185101']),
acquisitionDateTime: DICOMWeb.getString(instance['0008002A']),
numberOfFrames: DICOMWeb.getNumber(instance['00280008']),
frameIncrementPointer: getFrameIncrementPointer(instance['00280009']),
frameTime: DICOMWeb.getNumber(instance['00181063']),
frameTimeVector: parseFloatArray(
DICOMWeb.getString(instance['00181065'])
),
sliceThickness: DICOMWeb.getNumber(instance['00180050']),
spacingBetweenSlices: DICOMWeb.getString(instance['00180088']),
lossyImageCompression: DICOMWeb.getString(instance['00282110']),
derivationDescription: DICOMWeb.getString(instance['00282111']),
lossyImageCompressionRatio: DICOMWeb.getString(instance['00282112']),
lossyImageCompressionMethod: DICOMWeb.getString(instance['00282114']),
echoNumber: DICOMWeb.getString(instance['00180086']),
contrastBolusAgent: DICOMWeb.getString(instance['00180010']),
radiopharmaceuticalInfo: getRadiopharmaceuticalInfo(instance),
baseWadoRsUri: baseWadoRsUri,
wadouri: WADOProxy.convertURL(wadouri, server),
wadorsuri: WADOProxy.convertURL(wadorsuri, server),
wadoRoot: server.wadoRoot,
imageRendering: server.imageRendering,
thumbnailRendering: server.thumbnailRendering,
};
// Get additional information if the instance uses "PALETTE COLOR" photometric interpretation
if (instanceSummary.photometricInterpretation === 'PALETTE COLOR') {
const redPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281101'])
);
const greenPaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281102'])
);
const bluePaletteColorLookupTableDescriptor = parseFloatArray(
DICOMWeb.getString(instance['00281103'])
);
const palettes = await getPaletteColors(
server,
instance,
redPaletteColorLookupTableDescriptor
);
if (palettes) {
if (palettes.uid) {
instanceSummary.paletteColorLookupTableUID = palettes.uid;
}
instanceSummary.redPaletteColorLookupTableData = palettes.red;
instanceSummary.greenPaletteColorLookupTableData = palettes.green;
instanceSummary.bluePaletteColorLookupTableData = palettes.blue;
instanceSummary.redPaletteColorLookupTableDescriptor = redPaletteColorLookupTableDescriptor;
instanceSummary.greenPaletteColorLookupTableDescriptor = greenPaletteColorLookupTableDescriptor;
instanceSummary.bluePaletteColorLookupTableDescriptor = bluePaletteColorLookupTableDescriptor;
}
}
series.instances.push(instanceSummary);
})
);
return studyData;
throw new Error('Failed to create study out of provided SOP instance list');
}
/**
* Retrieve Study MetaData from a DICOM server using a WADO call
*
* @param server
* @param studyInstanceUid
* @returns {Promise}
* Retrieve Study metadata from a DICOM server. If the server is configured to use lazy load, only the first series
* will be loaded and the property "studyLoader" will be set to let consumer load remaining series as needed
* @param {Object} server Object with server configuration paramenters
* @param {string} studyInstanceUid The Study Instance UID of the study which needs to be loaded
* @returns {Object} A study descriptor object
*/
async function RetrieveMetadata(server, studyInstanceUid) {
const config = {
return (server.enableStudyLazyLoad
? lazyLoadStudyMetadata
: loadStudyMetadata)(server, studyInstanceUid);
}
/**
*
* @param {*} server
* @param {*} studyInstanceUID
*/
async function loadStudyMetadata(server, studyInstanceUID) {
const dicomWeb = new api.DICOMwebClient({
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
};
const dicomWeb = new api.DICOMwebClient(config);
const options = {
studyInstanceUID: studyInstanceUid,
};
});
return dicomWeb
.retrieveStudyMetadata({ studyInstanceUID })
.then(result => createStudyFromSOPInstanceList(server, result));
}
return dicomWeb.retrieveStudyMetadata(options).then(result => {
return resultDataToStudyMetadata(server, studyInstanceUid, result);
async function lazyLoadStudyMetadata(server, studyInstanceUid) {
const seriesInstanceUids = await searchStudySeries(server, studyInstanceUid);
const dicomWeb = new api.DICOMwebClient({
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
const seriesLoader = makeSeriesLoader(
dicomWeb,
studyInstanceUid,
seriesInstanceUids
);
const firstSeries = await seriesLoader.next();
const study = await createStudyFromSOPInstanceList(
server,
firstSeries.sopInstances
);
if (seriesLoader.hasNext()) {
attachSeriesLoader(server, study, seriesLoader);
}
return study;
}
function attachSeriesLoader(server, study, seriesLoader) {
study.seriesLoader = Object.freeze({
hasNext() {
return seriesLoader.hasNext();
},
async next() {
const series = await seriesLoader.next();
await addInstancesToStudy(server, study, series.sopInstances);
return study.seriesMap[series.seriesInstanceUID];
},
});
}
/**
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
* @param {string} studyInstanceUID The Study Instance UID from which series will be loaded
* @param {Array} seriesInstanceUIDList A list of Series Instance UIDs
* @returns {Object} Returns an object which supports loading of instances from each of given Series Instance UID
*/
function makeSeriesLoader(
dicomWebClient,
studyInstanceUID,
seriesInstanceUIDList
) {
return Object.freeze({
hasNext() {
return seriesInstanceUIDList.length > 0;
},
async next() {
const seriesInstanceUID = seriesInstanceUIDList.shift();
const sopInstances = await dicomWebClient.retrieveSeriesMetadata({
studyInstanceUID,
seriesInstanceUID,
});
return { studyInstanceUID, seriesInstanceUID, sopInstances };
},
});
}
/**
* Search series of a given study
* @param {Object} server Object with server configuration paramenters
* @param {string} studyInstanceUID The Study Instance UID to search series from;
* @returns {Arrays} A list of Series Instance UIDs
*/
async function searchStudySeries(server, studyInstanceUID) {
const dicomWeb = new api.DICOMwebClient({
url: server.qidoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
const seriesList = await dicomWeb.searchForSeries({ studyInstanceUID });
return seriesList
.sort(seriesSortingCriteria)
.map(series => getSeriesInfo(series).seriesInstanceUid);
}
/**
* Series sorting criteria: series considered low priority are moved to the end
* of the list and series number is used to break ties
* @param {Object} firstSeries
* @param {Object} secondSeries
*/
function seriesSortingCriteria(firstSeries, secondSeries) {
const a = getSeriesInfo(firstSeries);
const b = getSeriesInfo(secondSeries);
if (!a.isLowPriority && b.isLowPriority) {
return -1;
}
if (a.isLowPriority && !b.isLowPriority) {
return 1;
}
return a.seriesNumber - b.seriesNumber;
}
/**
* Creates an object with processed series information and saves its reference
* inside the series object itself to simplify sorting
* @param {Object} series The raw series object
*/
function getSeriesInfo(series) {
let info = series[INFO];
if (!info) {
const modality = DICOMWeb.getString(series['00080060'], '').toUpperCase();
info = Object.freeze({
modality,
isLowPriority: isLowPriorityModality(modality),
seriesInstanceUid: DICOMWeb.getString(series['0020000E']),
seriesNumber: DICOMWeb.getNumber(series['00200011'], 0) || 0,
});
series[INFO] = info;
}
return info;
}
export default RetrieveMetadata;
@@ -0,0 +1,5 @@
const LOW_PRIORITY_MODALITIES = Object.freeze(['SEG', 'KO', 'PR']);
export default function isLowPriorityModality(modality) {
return LOW_PRIORITY_MODALITIES.includes(modality);
}
@@ -70,32 +70,48 @@ const getWadoRsInstanceMetaData = (study, series, instance) => {
.toJSON();
};
export default function updateMetaDataManager(study) {
study.seriesList.forEach(series => {
series.instances.forEach(instance => {
// Cache just images that are going to be loaded via WADO-RS
if (
instance.imageRendering !== 'wadors' &&
instance.thumbnailRendering !== 'wadors'
) {
return;
}
/**
* Update metadata manager with instances of a specifc series
* @param {Object} study A plain study descriptor object
* @param {Object} series A Series descriptor object contaning the instances to be added to the manager
*/
function updateMetaDataManagerForSeries(study, series) {
series.instances.forEach(instance => {
// Cache just images that are going to be loaded via WADO-RS
if (
instance.imageRendering !== 'wadors' &&
instance.thumbnailRendering !== 'wadors'
) {
return;
}
const metaData = getWadoRsInstanceMetaData(study, series, instance);
const numberOfFrames = instance.numberOfFrames || 1;
const metaData = getWadoRsInstanceMetaData(study, series, instance);
const numberOfFrames = instance.numberOfFrames || 1;
// We can share the same metaData with all frames because it doesn't have
// any frame specific data, such as frameNumber, pixelData, offset, etc.
// WADO-RS frame number is 1-based
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
const imageId = getWADORSImageId(instance, frameNumber);
// We can share the same metaData with all frames because it doesn't have
// any frame specific data, such as frameNumber, pixelData, offset, etc.
// WADO-RS frame number is 1-based
for (let frameNumber = 0; frameNumber < numberOfFrames; frameNumber++) {
const imageId = getWADORSImageId(instance, frameNumber);
// TODO Drop dependency on this
cornerstoneWADOImageLoader.wadors.metaDataManager.add(
imageId,
metaData
);
}
});
// TODO Drop dependency on this
cornerstoneWADOImageLoader.wadors.metaDataManager.add(imageId, metaData);
}
});
}
/**
* Update metadata manager
* @param {Object} study A plain study descriptor object
* @param {string} [seriesInstanceUid] The Series Instance UID of the series to be added (Optional)
*/
export default function updateMetaDataManager(study, seriesInstanceUid) {
if (seriesInstanceUid) {
const series = study.seriesMap[seriesInstanceUid];
updateMetaDataManagerForSeries(study, series);
} else {
study.seriesList.forEach(series => {
updateMetaDataManagerForSeries(study, series);
});
}
}
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.53.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.52.0...@ohif/ui@0.53.0) (2019-10-03)
### Features
* Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953)) ([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b))
# [0.52.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.3...@ohif/ui@0.52.0) (2019-10-01)
### Features
* 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle ([#947](https://github.com/OHIF/Viewers/issues/947)) ([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3))
## [0.51.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.51.2...@ohif/ui@0.51.3) (2019-10-01)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/ui",
"version": "0.51.3",
"version": "0.53.0",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
@@ -6,15 +6,29 @@ export class Checkbox extends Component {
static propTypes = {
label: PropTypes.string.isRequired,
checked: PropTypes.bool,
onChange: PropTypes.func,
};
constructor(props) {
super(props);
this.state = { checked: props.checked, label: props.label };
this.state = { checked: !!props.checked, label: props.label };
}
handleChange(e) {
this.setState({ checked: e.target.checked });
const checked = e.target.checked;
this.setState({ checked });
if (this.props.onChange) this.props.onChange(checked);
}
componentDidUpdate(props) {
const { checked = false, label } = props;
if (this.state.checked !== checked || this.state.label !== label) {
this.setState({
checked,
label,
});
}
}
render() {
@@ -32,9 +46,7 @@ export class Checkbox extends Component {
<input
type="checkbox"
checked={this.state.checked}
onChange={e => {
this.handleChange(e);
}}
onChange={this.handleChange.bind(this)}
/>
{checkbox}
{this.state.label}
+2
View File
@@ -13,6 +13,7 @@ import {
UserPreferencesModal,
} from './userPreferencesModal';
import { Checkbox } from './checkbox';
import { CineDialog } from './cineDialog';
import { QuickSwitch } from './quickSwitch';
import { RoundedButtonGroup } from './roundedButtonGroup';
@@ -23,6 +24,7 @@ import { ToolbarSection } from './toolbarSection';
import { Tooltip } from './tooltip';
export {
Checkbox,
CineDialog,
ExampleDropTarget,
LayoutButton,
@@ -1,3 +1,4 @@
/* global cornerstone */
import './ImageThumbnail.styl';
import React, { PureComponent } from 'react';
@@ -12,25 +13,6 @@ import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator';
// - Pass in the entire load and render function as a prop?
//import cornerstone from 'cornerstone-core';
/**
* Asynchronous wrapper around Cornerstone's renderToCanvas method.
*
* @param {HTMLElement} canvasElement An HTML <canvas> element
* @param {Image} image A Cornerstone Image
*
* @return {Promise} A promise tracking the progress of the rendering. Resolves empty.
*/
function renderAsync(canvasElement, image) {
return new Promise((resolve, reject) => {
try {
cornerstone.renderToCanvas(canvasElement, image);
resolve();
} catch (error) {
reject(error);
}
});
}
export default class ImageThumbnail extends PureComponent {
static propTypes = {
imageSrc: PropTypes.string,
@@ -52,40 +34,32 @@ export default class ImageThumbnail extends PureComponent {
super(props);
this.canvas = React.createRef();
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
this.state = {
loading: renderIntoCanvas,
loading: this.shouldRenderToCanvas(),
};
}
shouldRenderToCanvas() {
return this.props.imageId && !this.props.imageSrc;
}
componentDidMount() {
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
if (renderIntoCanvas) {
const { imageId } = this.props;
const canvas = this.canvas.current;
cornerstone.loadAndCacheImage(imageId).then(
image => {
renderAsync(canvas, image).then(
() => {
this.setState({
loading: false,
});
},
error => {
// TODO: Set state?
throw new Error(error);
}
);
},
error => {
// TODO: Set state?
if (this.shouldRenderToCanvas()) {
cornerstone
.loadAndCacheImage(this.props.imageId)
.then(image => {
cornerstone.renderToCanvas(this.canvas.current, image);
this.setState({
loading: false,
});
})
.catch(error => {
this.setState({
loading: false,
error: true,
});
throw new Error(error);
}
);
});
}
}
@@ -100,12 +74,10 @@ export default class ImageThumbnail extends PureComponent {
const showStackLoadingProgressBar =
this.props.stackPercentComplete !== undefined;
const renderIntoCanvas = this.props.imageId && !this.props.imageSrc;
return (
<div className="ImageThumbnail">
<div className="image-thumbnail-canvas">
{renderIntoCanvas ? (
{this.shouldRenderToCanvas() ? (
<canvas
ref={this.canvas}
width={this.props.width}
@@ -130,6 +102,9 @@ export default class ImageThumbnail extends PureComponent {
/>
</div>
)}
{this.state.loading && (
<div className="image-thumbnail-loading-indicator"></div>
)}
</div>
);
}
@@ -26,6 +26,36 @@
-webkit-background-clip: padding
background-clip: padding-box
// Thumbnail Loading Indicator
.image-thumbnail-loading-indicator
position: absolute
left: 50%
top: 50%
margin: -22px 0 0 -22px
display: block
width: 44px
height: 44px
&:after
content: " "
display: block
width: 32px
height: 32px
margin: 1px
border-radius: 50%
border: 5px solid var(--ui-border-color-dark)
border-color: var(--ui-border-color-dark) transparent var(--ui-border-color-dark) transparent
animation: image-thumbnail-loading-indicator 1.2s linear infinite
@keyframes image-thumbnail-loading-indicator
0%
transform: rotate(0deg)
100%
transform: rotate(360deg)
// The End...
.image-thumbnail-canvas
height: 100%
overflow: hidden
@@ -36,7 +66,6 @@
-webkit-user-drag: none
pointer-events: none
.thumbnailLoadingIndicator
display: none
pointer-events: none
@@ -25,7 +25,7 @@ class StudyBrowser extends Component {
if (this.props.supportsDragAndDrop) {
return (
<ThumbnailEntryDragSource
key={`${studyIndex}_${thumbIndex}`}
key={thumb.displaySetInstanceUid}
{...study}
{...thumb}
id={`${studyIndex}_${thumbIndex}`}
@@ -37,7 +37,7 @@ class StudyBrowser extends Component {
return (
<div className="ThumbnailEntryContainer">
<ThumbnailEntry
key={`${studyIndex}_${thumbIndex}`}
key={thumb.displaySetInstanceUid}
{...study}
{...thumb}
id={`${studyIndex}_${thumbIndex}`}
+1
View File
@@ -2,6 +2,7 @@
margin: 0;
width: 100%;
-webkit-appearance: none;
background-color: transparent;
}
.range:focus {
+2 -1
View File
@@ -11,7 +11,7 @@ class Range extends React.Component {
handleChange = event => {
this.setState({ value: event.target.value });
if (this.props.onChange) this.props.onChange();
if (this.props.onChange) this.props.onChange(event);
};
render() {
@@ -21,6 +21,7 @@ class Range extends React.Component {
value={this.state.value}
min={this.props.min}
max={this.props.max}
step={this.props.step || 1}
onChange={this.handleChange}
id={this.props.id}
className="range"
-4
View File
@@ -34,10 +34,6 @@
}
.select-ohif:focus {
border-color: #aaa;
box-shadow: 0 0 1px 3px rgba(59, 153, 252, 0.7);
box-shadow: 0 0 0 3px -moz-mac-focusring;
color: #222;
outline: none;
}
+3 -2
View File
@@ -11,8 +11,9 @@ class Select extends Component {
}
handleChange = event => {
this.setState({ value: event.target.value });
if (this.props.onChange) this.props.onChange();
const value = event.target.value;
this.setState({ value });
if (this.props.onChange) this.props.onChange(value);
};
render() {
+5 -1
View File
@@ -1,4 +1,5 @@
import {
Checkbox,
CineDialog,
ExampleDropTarget,
LayoutButton,
@@ -25,7 +26,7 @@ import {
import { ICONS, Icon } from './elements';
// Alias this for now as not all dependents are using strict versioning
import { DropdownMenu as Dropdown } from './elements/form';
import { DropdownMenu as Dropdown, Range, Select } from './elements/form';
import ExpandableToolMenu from './viewer/ExpandableToolMenu.js';
import LayoutManager from './LayoutChooser/LayoutManager.js';
import LayoutPanelDropTarget from './LayoutChooser/LayoutPanelDropTarget.js';
@@ -38,6 +39,7 @@ import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js';
export {
ICONS,
//
Checkbox,
CineDialog,
Dropdown,
ExpandableToolMenu,
@@ -53,8 +55,10 @@ export {
OverlayTrigger,
PlayClipButton,
QuickSwitch,
Range,
RoundedButtonGroup,
ScrollableArea,
Select,
SelectTree,
SimpleDialog,
StudyBrowser,
+18 -10
View File
@@ -108,16 +108,8 @@ class ExpandableToolMenu extends React.Component {
};
render() {
return (
<OverlayTrigger
key="menu-button"
trigger="click"
placement="bottom"
rootClose={true}
handleHide={this.onOverlayHide}
onClick={this.onExpandableToolClick}
overlay={this.toolbarMenuOverlay()}
>
const getToolBarButtonComponent = () => {
return (
<ToolbarButton
key="menu-button"
type="tool"
@@ -128,6 +120,22 @@ class ExpandableToolMenu extends React.Component {
isExpandable={true}
isExpanded={this.state.isExpanded}
/>
);
};
const toolbarComponent = getToolBarButtonComponent();
return (
<OverlayTrigger
key="menu-button"
trigger="click"
placement="bottom"
rootClose={true}
handleHide={this.onOverlayHide}
onClick={this.onExpandableToolClick}
overlay={this.toolbarMenuOverlay()}
>
{toolbarComponent}
</OverlayTrigger>
);
}
+2
View File
@@ -114,6 +114,8 @@ module.exports = (env, argv) => {
hot: true,
open: true,
port: 3000,
host: '0.0.0.0',
public: 'http://localhost:' + 3000,
historyApiFallback: {
disableDotRule: true,
},
+57
View File
@@ -3,6 +3,63 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.4.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.3...@ohif/viewer@1.4.0) (2019-10-03)
### Features
* Use QIDO + WADO to load series metadata individually rather than the entire study metadata at once ([#953](https://github.com/OHIF/Viewers/issues/953)) ([9e10c2b](https://github.com/OHIF/Viewers/commit/9e10c2b))
## [1.3.3](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.2...@ohif/viewer@1.3.3) (2019-10-02)
**Note:** Version bump only for package @ohif/viewer
## [1.3.2](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.1...@ohif/viewer@1.3.2) (2019-10-01)
**Note:** Version bump only for package @ohif/viewer
## [1.3.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.3.0...@ohif/viewer@1.3.1) (2019-10-01)
### Bug Fixes
* Exit MPR mode if Layout is changed ([#984](https://github.com/OHIF/Viewers/issues/984)) ([674ca9f](https://github.com/OHIF/Viewers/commit/674ca9f))
# [1.3.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.8...@ohif/viewer@1.3.0) (2019-10-01)
### Features
* 🎸 MPR UI improvements. Added MinIP, AvgIP, slab thickness slider and mode toggle ([#947](https://github.com/OHIF/Viewers/issues/947)) ([c79c0c3](https://github.com/OHIF/Viewers/commit/c79c0c3))
## [1.2.8](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.7...@ohif/viewer@1.2.8) (2019-10-01)
**Note:** Version bump only for package @ohif/viewer
## [1.2.7](https://github.com/OHIF/Viewers/compare/@ohif/viewer@1.2.6...@ohif/viewer@1.2.7) (2019-10-01)
**Note:** Version bump only for package @ohif/viewer
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@ohif/viewer",
"version": "1.2.7",
"version": "1.4.0",
"description": "OHIF Viewer",
"author": "OHIF Contributors",
"license": "MIT",
@@ -46,14 +46,14 @@
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/core": "^1.0.1",
"@ohif/core": "^1.1.0",
"@ohif/extension-cornerstone": "^2.0.0",
"@ohif/extension-dicom-html": "^1.0.0",
"@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.0",
"@ohif/extension-vtk": "^0.50.23",
"@ohif/extension-vtk": "^0.51.3",
"@ohif/i18n": "^0.50.4",
"@ohif/ui": "^0.51.3",
"@ohif/ui": "^0.53.0",
"@tanem/react-nprogress": "^1.1.25",
"classnames": "^2.2.6",
"core-js": "^3.2.1",
+1
View File
@@ -13,6 +13,7 @@ window.config = {
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
},
],
},
+1
View File
@@ -12,6 +12,7 @@ window.config = {
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
},
],
},
+1
View File
@@ -11,6 +11,7 @@ window.config = {
qidoSupportsIncludeField: true,
imageRendering: 'wadors',
thumbnailRendering: 'wadors',
enableStudyLazyLoad: true,
},
],
},
@@ -13,15 +13,23 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => {
return {
// TODO: Change if layout switched becomes more complex
onChange: selectedCell => {
onChange: (selectedCell, currentLayout) => {
let viewports = [];
const rows = selectedCell.row + 1;
const columns = selectedCell.col + 1;
const numViewports = rows * columns;
for (let i = 0; i < numViewports; i++) {
// Hacky way to allow users to exit MPR "mode"
const viewport = currentLayout.viewports[i];
let plugin = viewport && viewport.plugin;
if (viewport && viewport.vtk) {
plugin = 'cornerstone';
}
viewports.push({
height: `${100 / rows}%`,
width: `${100 / columns}%`,
plugin,
});
}
const layout = {
@@ -33,9 +41,19 @@ const mapDispatchToProps = dispatch => {
};
};
const mergeProps = (propsFromState, propsFromDispatch) => {
const onChangeFromDispatch = propsFromDispatch.onChange;
const { currentLayout } = propsFromState;
return {
onChange: (selectedCell) => onChangeFromDispatch(selectedCell, currentLayout)
};
}
const ConnectedLayoutButton = connect(
mapStateToProps,
mapDispatchToProps
mapDispatchToProps,
mergeProps
)(LayoutButton);
export default ConnectedLayoutButton;
@@ -229,6 +229,7 @@ function _getButtonComponents(toolbarButtons, activeButtons) {
});
}
/**
* A handy way for us to handle different button types. IE. firing commands for
* buttons, or initiation built in behavior.
@@ -1,11 +1,11 @@
import React, { Component } from "react";
import { metadata, studies, utils } from "@ohif/core";
import React, { Component } from 'react';
import { metadata, studies, utils, log } from '@ohif/core';
import ConnectedViewer from "./ConnectedViewer.js";
import PropTypes from "prop-types";
import { extensionManager } from "./../App.js";
import ConnectedViewer from './ConnectedViewer.js';
import PropTypes from 'prop-types';
import { extensionManager } from './../App.js';
const { OHIFStudyMetadata } = metadata;
const { OHIFStudyMetadata, OHIFSeriesMetadata } = metadata;
const { retrieveStudiesMetadata } = studies;
const { studyMetadataManager, updateMetaDataManager } = utils;
@@ -13,65 +13,97 @@ class ViewerRetrieveStudyData extends Component {
static propTypes = {
studyInstanceUids: PropTypes.array.isRequired,
seriesInstanceUids: PropTypes.array,
server: PropTypes.object
server: PropTypes.object,
};
state = {
studies: null,
error: null
};
async componentDidMount() {
// TODO: Avoid using timepoints here
//const params = { studyInstanceUids, seriesInstanceUids, timepointId, timepointsFilter={} };
const { studyInstanceUids, seriesInstanceUids, server } = this.props;
constructor(props) {
super(props);
this.state = {
studies: null,
error: null,
};
}
async loadStudies() {
try {
const { server, studyInstanceUids, seriesInstanceUids } = this.props;
const studies = await retrieveStudiesMetadata(
server,
studyInstanceUids,
seriesInstanceUids
);
this.setStudies(studies);
} catch (e) {
this.setState({ error: true });
log.error(e);
}
}
// Render the viewer when the data is ready
// TODO: CLEAR THIS SOMEWHERE ELSE
studyMetadataManager.purge();
setStudies(givenStudies) {
if (Array.isArray(givenStudies) && givenStudies.length > 0) {
const sopClassHandlerModules =
extensionManager.modules['sopClassHandlerModule'];
// Map studies to new format, update metadata manager?
const updatedStudies = studies.map(study => {
const studies = givenStudies.map(study => {
const studyMetadata = new OHIFStudyMetadata(
study,
study.studyInstanceUid
);
const sopClassHandlerModules =
extensionManager.modules["sopClassHandlerModule"];
study.displaySets =
study.displaySets ||
studyMetadata.createDisplaySets(sopClassHandlerModules);
if (!study.displaySets) {
study.displaySets = studyMetadata.createDisplaySets(
sopClassHandlerModules
);
}
studyMetadata.setDisplaySets(study.displaySets);
// Updates WADO-RS metaDataManager
updateMetaDataManager(study);
studyMetadataManager.add(studyMetadata);
// Attempt to load remaning series if any
this._attemptToLoadRemainingSeries(studyMetadata);
return study;
});
this.setState({
studies: updatedStudies
});
} catch (err) {
this.setState({
error: true
});
// TODO: Handle gracefully instead of throwing?
throw new Error(err);
this.setState({ studies });
}
}
_addSeriesToStudy(studyMetadata, series) {
const sopClassHandlerModules =
extensionManager.modules['sopClassHandlerModule'];
const study = studyMetadata.getData();
const seriesMetadata = new OHIFSeriesMetadata(series, study);
studyMetadata.addSeries(seriesMetadata);
studyMetadata.createAndAddDisplaySetsForSeries(
sopClassHandlerModules,
seriesMetadata
);
study.displaySets = studyMetadata.getDisplaySets();
updateMetaDataManager(study, series.seriesInstanceUid);
this.setState(function(state) {
return { studies: state.studies.slice() };
});
}
_attemptToLoadRemainingSeries(studyMetadata) {
const { seriesLoader } = studyMetadata.getData();
if (!seriesLoader) {
return;
}
while (seriesLoader.hasNext()) {
seriesLoader
.next()
.then(
series => void this._addSeriesToStudy(studyMetadata, series),
error => void log.error(error)
);
}
}
componentDidMount() {
// TODO: CLEAR THIS SOMEWHERE ELSE
studyMetadataManager.purge();
this.loadStudies();
}
render() {
if (this.state.error) {
return <div>Error: {JSON.stringify(this.state.error)}</div>;
+10 -4
View File
@@ -8273,6 +8273,11 @@ gl-matrix@3.0.0:
resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.0.0.tgz#888301ac7650e148c3865370e13ec66d08a8381f"
integrity sha512-PD4mVH/C/Zs64kOozeFnKY8ybhgwxXXQYGWdB4h68krAHknWJgk9uKOn6z8YElh5//vs++90pb6csrTIDWnexA==
gl-matrix@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.1.0.tgz#f5b2de17d8fed95a79e5025b10cded0ab9ccbed0"
integrity sha512-526NA+3EA+ztAQi0IZpSWiM0fyQXIp7IbRvfJ4wS/TjjQD0uv0fVybXwwqqSOlq33UckivI0yMDlVtboWm3k7A==
gl-preserve-state@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/gl-preserve-state/-/gl-preserve-state-1.0.0.tgz#4ef710d62873f1470ed015c6546c37dacddd4198"
@@ -14954,12 +14959,13 @@ react-transition-group@^4.1.1:
loose-envify "^1.4.0"
prop-types "^15.6.2"
react-vtkjs-viewport@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/react-vtkjs-viewport/-/react-vtkjs-viewport-0.1.0.tgz#a00b114b635ff95a82ae1227d70c248e4cafddf4"
integrity sha512-0gnRfntpV6ZBNvZOHzjHapFeJajO8nfkSvO5SNUu41bkyXxqYcJwZvb3EWbd04V1RnA0AnKRoR/KL+I09Os7gg==
react-vtkjs-viewport@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/react-vtkjs-viewport/-/react-vtkjs-viewport-0.1.2.tgz#087af73b1c46627c5079d25ab1b0d3866cb7b06b"
integrity sha512-1UejtY8i6LhK2FXouD7so2VRYbdjo7w80VUQ6EesyurBAZYAYW3ppvAlBjtvMlJhYZn13wwE3gTFAbrHPVPjDw==
dependencies:
date-fns "^2.2.1"
gl-matrix "^3.1.0"
react-with-direction@1.3.0:
version "1.3.0"