Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70672ed663 | ||
|
|
858484f250 | ||
|
|
83f9478977 | ||
|
|
b5b851ac7c | ||
|
|
e5175bf3e6 | ||
|
|
8c8f69accb | ||
|
|
f9037f831d | ||
|
|
9e0bd52c6a |
No files matched your search
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.5.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.1...@ohif/extension-cornerstone@2.5.2) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.0...@ohif/extension-cornerstone@2.5.1) (2020-04-02)
|
## [2.5.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.5.0...@ohif/extension-cornerstone@2.5.1) (2020-04-02)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-cornerstone",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "2.5.1",
|
"version": "2.5.2",
|
||||||
"description": "OHIF extension for Cornerstone",
|
"description": "OHIF extension for Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import { getEnabledElement } from './state';
|
|||||||
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
import CornerstoneViewportDownloadForm from './CornerstoneViewportDownloadForm';
|
||||||
const scroll = cornerstoneTools.import('util/scroll');
|
const scroll = cornerstoneTools.import('util/scroll');
|
||||||
|
|
||||||
|
const { studyMetadataManager } = OHIF.utils;
|
||||||
|
const { setViewportSpecificData } = OHIF.redux.actions;
|
||||||
|
|
||||||
const commandsModule = ({ servicesManager }) => {
|
const commandsModule = ({ servicesManager }) => {
|
||||||
const actions = {
|
const actions = {
|
||||||
rotateViewport: ({ viewports, rotation }) => {
|
rotateViewport: ({ viewports, rotation }) => {
|
||||||
@@ -255,9 +258,35 @@ const commandsModule = ({ servicesManager }) => {
|
|||||||
cornerstone.setViewport(enabledElement, viewport);
|
cornerstone.setViewport(enabledElement, viewport);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
jumpToImage: ({
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameIndex,
|
||||||
|
activeViewportIndex
|
||||||
|
}) => {
|
||||||
|
const study = studyMetadataManager.get(StudyInstanceUID);
|
||||||
|
|
||||||
|
const displaySet = study.findDisplaySet(ds => {
|
||||||
|
return ds.images.find(i => i.getSOPInstanceUID() === SOPInstanceUID)
|
||||||
|
});
|
||||||
|
|
||||||
|
displaySet.SOPInstanceUID = SOPInstanceUID;
|
||||||
|
displaySet.frameIndex = frameIndex;
|
||||||
|
|
||||||
|
window.store.dispatch(setViewportSpecificData(activeViewportIndex, displaySet));
|
||||||
|
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
cornerstone.updateImage(enabledElement.element);
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const definitions = {
|
const definitions = {
|
||||||
|
jumpToImage: {
|
||||||
|
commandFn: actions.jumpToImage,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
getNearbyToolData: {
|
getNearbyToolData: {
|
||||||
commandFn: actions.getNearbyToolData,
|
commandFn: actions.getNearbyToolData,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const webpack = require('webpack');
|
||||||
|
const merge = require('webpack-merge');
|
||||||
|
const path = require('path');
|
||||||
|
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||||
|
const pkg = require('./../package.json');
|
||||||
|
|
||||||
|
const ROOT_DIR = path.join(__dirname, './..');
|
||||||
|
const SRC_DIR = path.join(__dirname, '../src');
|
||||||
|
const DIST_DIR = path.join(__dirname, '../dist');
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
|
|
||||||
|
return merge(commonConfig, {
|
||||||
|
devtool: 'source-map',
|
||||||
|
stats: {
|
||||||
|
colors: true,
|
||||||
|
hash: true,
|
||||||
|
timings: true,
|
||||||
|
assets: true,
|
||||||
|
chunks: false,
|
||||||
|
chunkModules: false,
|
||||||
|
modules: false,
|
||||||
|
children: false,
|
||||||
|
warnings: true,
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: true,
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: ROOT_DIR,
|
||||||
|
library: 'OHIFExtDicomSeg',
|
||||||
|
libraryTarget: 'umd',
|
||||||
|
libraryExport: 'default',
|
||||||
|
filename: pkg.main,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new webpack.optimize.LimitChunkCountPlugin({
|
||||||
|
maxChunks: 1,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## 0.0.2 (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Open Health Imaging Foundation
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# @ohif/extension-dicom-rt
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "@ohif/extension-dicom-rt",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
||||||
|
"author": "OHIF",
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": "OHIF/Viewers",
|
||||||
|
"main": "dist/index.umd.js",
|
||||||
|
"module": "src/index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1.16.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"README.md"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||||
|
"dev:dicom-html": "yarn run dev",
|
||||||
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
|
"build:package": "yarn run build",
|
||||||
|
"prepublishOnly": "yarn run build",
|
||||||
|
"start": "yarn run dev"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@ohif/core": "^0.50.0",
|
||||||
|
"cornerstone-core": "^2.2.8",
|
||||||
|
"cornerstone-tools": "^4.0.9",
|
||||||
|
"dcmjs": "^0.8.3",
|
||||||
|
"prop-types": "^15.6.2",
|
||||||
|
"react": "^16.8.6",
|
||||||
|
"react-dom": "^16.8.6"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"react-select": "^3.0.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import { MODULE_TYPES, utils, DICOMWeb } from '@ohif/core';
|
||||||
|
import loadRTStruct from './loadRTStruct';
|
||||||
|
|
||||||
|
import id from './id';
|
||||||
|
|
||||||
|
// TODO: Should probably use dcmjs for this
|
||||||
|
const SOP_CLASS_UIDS = {
|
||||||
|
DICOM_RT_STRUCT: '1.2.840.10008.5.1.4.1.1.481.3',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||||
|
|
||||||
|
const OHIFDicomRTStructSopClassHandler = {
|
||||||
|
id: 'OHIFDicomRTStructSopClassHandler',
|
||||||
|
type: MODULE_TYPES.SOP_CLASS_HANDLER,
|
||||||
|
sopClassUIDs,
|
||||||
|
getDisplaySetFromSeries: function (
|
||||||
|
series,
|
||||||
|
study,
|
||||||
|
dicomWebClient,
|
||||||
|
authorizationHeaders
|
||||||
|
) {
|
||||||
|
const instance = series.getFirstInstance();
|
||||||
|
|
||||||
|
const metadata = instance.getData().metadata;
|
||||||
|
const {
|
||||||
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
|
SeriesDescription,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
} = metadata;
|
||||||
|
|
||||||
|
// TODO -> GET REFERENCED FRAME OF REFERENCE SEQUENCE.
|
||||||
|
|
||||||
|
const rtStructDisplaySet = {
|
||||||
|
Modality: 'RTSTRUCT',
|
||||||
|
displaySetInstanceUID: utils.guid(),
|
||||||
|
wadoRoot: study.getData().wadoRoot,
|
||||||
|
wadoUri: instance.getData().wadouri,
|
||||||
|
SOPInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
authorizationHeaders,
|
||||||
|
metadata,
|
||||||
|
isDerived: true,
|
||||||
|
referencedDisplaySetUID: null, // Assigned when loaded.
|
||||||
|
labelmapIndex: null, // Assigned when loaded.
|
||||||
|
isLoaded: false,
|
||||||
|
SeriesDate,
|
||||||
|
SeriesTime,
|
||||||
|
SeriesDescription,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!metadata.ReferencedSeriesSequence) {
|
||||||
|
const ReferencedFrameOfReferenceSequence =
|
||||||
|
metadata.ReferencedFrameOfReferenceSequence;
|
||||||
|
|
||||||
|
if (ReferencedFrameOfReferenceSequence) {
|
||||||
|
// TODO -> @dannyrb Do we augment metadata or add a (potentially large? fallback list in filterDerivedDisplaySets )
|
||||||
|
metadata.ReferencedSeriesSequence = _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||||
|
ReferencedFrameOfReferenceSequence
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rtStructDisplaySet.load = function (referencedDisplaySet, studies) {
|
||||||
|
return loadRTStruct(
|
||||||
|
rtStructDisplaySet,
|
||||||
|
referencedDisplaySet,
|
||||||
|
studies
|
||||||
|
).catch(error => {
|
||||||
|
rtStructDisplaySet.isLoaded = false;
|
||||||
|
throw new Error(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return rtStructDisplaySet;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function _deriveReferencedSeriesSequenceFromFrameOfReferenceSequence(
|
||||||
|
ReferencedFrameOfReferenceSequence
|
||||||
|
) {
|
||||||
|
const ReferencedSeriesSequence = [];
|
||||||
|
|
||||||
|
_getSequenceAsArray(ReferencedFrameOfReferenceSequence).forEach(
|
||||||
|
referencedFrameOfReference => {
|
||||||
|
const { RTReferencedStudySequence } = referencedFrameOfReference;
|
||||||
|
|
||||||
|
_getSequenceAsArray(RTReferencedStudySequence).forEach(
|
||||||
|
rtReferencedStudy => {
|
||||||
|
const { RTReferencedSeriesSequence } = rtReferencedStudy;
|
||||||
|
|
||||||
|
_getSequenceAsArray(RTReferencedSeriesSequence).forEach(
|
||||||
|
rtReferencedSeries => {
|
||||||
|
const ReferencedInstanceSequence = [];
|
||||||
|
const {
|
||||||
|
ContourImageSequence,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
} = rtReferencedSeries;
|
||||||
|
|
||||||
|
_getSequenceAsArray(ContourImageSequence).forEach(
|
||||||
|
contourImage => {
|
||||||
|
ReferencedInstanceSequence.push({
|
||||||
|
ReferencedSOPInstanceUID:
|
||||||
|
contourImage.ReferencedSOPInstanceUID,
|
||||||
|
ReferencedSOPClassUID: contourImage.ReferencedSOPClassUID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedSeries = {
|
||||||
|
SeriesInstanceUID,
|
||||||
|
ReferencedInstanceSequence,
|
||||||
|
};
|
||||||
|
|
||||||
|
ReferencedSeriesSequence.push(referencedSeries);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return ReferencedSeriesSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getSequenceAsArray(sequence) {
|
||||||
|
return Array.isArray(sequence) ? sequence : [sequence];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getReferencedFrameOfReferenceSequence(instance) {
|
||||||
|
const referencedFrameOfReferenceSequenceRaw = instance['30060010'];
|
||||||
|
|
||||||
|
const referencedFrameOfReferenceSequence = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!referencedFrameOfReferenceSequenceRaw ||
|
||||||
|
!referencedFrameOfReferenceSequenceRaw.Value
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
referencedFrameOfReferenceSequenceRaw.Value.forEach(
|
||||||
|
referencedFrameOfReferenceRaw => {
|
||||||
|
const frameOfReferenceUID = DICOMWeb.getString(
|
||||||
|
referencedFrameOfReferenceRaw['00200052']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedFrameOfReference = { frameOfReferenceUID };
|
||||||
|
const RTReferencedStudySequenceRaw =
|
||||||
|
referencedFrameOfReferenceRaw['30060012'];
|
||||||
|
|
||||||
|
referencedFrameOfReferenceSequence.push(referencedFrameOfReference);
|
||||||
|
|
||||||
|
if (RTReferencedStudySequenceRaw && RTReferencedStudySequenceRaw.Value) {
|
||||||
|
referencedFrameOfReference.rtReferencedStudySequence = [];
|
||||||
|
|
||||||
|
const { rtReferencedStudySequence } = referencedFrameOfReference;
|
||||||
|
|
||||||
|
RTReferencedStudySequenceRaw.Value.forEach(rtReferencedStudyRaw => {
|
||||||
|
const referencedSopClassUID = DICOMWeb.getString(
|
||||||
|
rtReferencedStudyRaw['00081150']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedSOPInstanceUID = DICOMWeb.getString(
|
||||||
|
rtReferencedStudyRaw['00081155']
|
||||||
|
);
|
||||||
|
|
||||||
|
const rtReferencedStudy = {
|
||||||
|
referencedSopClassUID,
|
||||||
|
referencedSOPInstanceUID,
|
||||||
|
rtReferencedSeriesSequence: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
rtReferencedStudySequence.push(rtReferencedStudy);
|
||||||
|
|
||||||
|
const { rtReferencedSeriesSequence } = rtReferencedStudy;
|
||||||
|
|
||||||
|
const rtReferencedSeriesSequenceRaw =
|
||||||
|
rtReferencedStudyRaw['30060014'];
|
||||||
|
|
||||||
|
rtReferencedSeriesSequenceRaw.Value.forEach(rtReferencedSeriesRaw => {
|
||||||
|
const seriesInstanceUID = DICOMWeb.getString(
|
||||||
|
rtReferencedSeriesRaw['0020000E']
|
||||||
|
);
|
||||||
|
|
||||||
|
const rtReferencedSeries = {
|
||||||
|
seriesInstanceUID,
|
||||||
|
contourImageSequence: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
rtReferencedSeriesSequence.push(rtReferencedSeries);
|
||||||
|
|
||||||
|
const { contourImageSequence } = rtReferencedSeries;
|
||||||
|
|
||||||
|
const contourImageSequenceRaw = rtReferencedSeriesRaw['30060016'];
|
||||||
|
|
||||||
|
contourImageSequenceRaw.Value.forEach(contourImageRaw => {
|
||||||
|
const referencedSOPClassUID = DICOMWeb.getString(
|
||||||
|
contourImageRaw['00081150']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedSOPInstanceUID = DICOMWeb.getString(
|
||||||
|
contourImageRaw['00081155']
|
||||||
|
);
|
||||||
|
|
||||||
|
const referencedFrameNumber = DICOMWeb.getString(
|
||||||
|
contourImageRaw['00081160']
|
||||||
|
);
|
||||||
|
const referencedSegmentNumber = DICOMWeb.getString(
|
||||||
|
contourImageRaw['0062000B']
|
||||||
|
);
|
||||||
|
|
||||||
|
const contourImage = {
|
||||||
|
referencedSOPClassUID,
|
||||||
|
referencedSOPInstanceUID,
|
||||||
|
referencedFrameNumber,
|
||||||
|
referencedSegmentNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
contourImageSequence.push(contourImage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return referencedFrameOfReferenceSequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OHIFDicomRTStructSopClassHandler;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const LoadingIndicator = ({ height = "200px", width = "200px", expand = false }) => {
|
||||||
|
return (
|
||||||
|
<div style={expand ? { height: "100%", width: "100%" } : {}}>
|
||||||
|
<svg style={{ margin: 'auto', background: 'none', display: 'block', shapeRendering: 'auto' }} width={width} height={height} viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
|
||||||
|
<g transform="rotate(0 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(30 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(60 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(90 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(120 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(150 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(180 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(210 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(240 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(270 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(300 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g><g transform="rotate(330 50 50)">
|
||||||
|
<rect x="49" y="24" rx="0" ry="0" width="2" height="12" fill="#20a5d6">
|
||||||
|
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
|
||||||
|
</rect>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LoadingIndicator;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
.dcmrt-panel-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: var(--ui-gray-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header .eye-icon,
|
||||||
|
.dcmrt-panel-section .header .angle-double-down,
|
||||||
|
.dcmrt-panel-section .header .angle-double-up {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header .eye-icon:hover,
|
||||||
|
.dcmrt-panel-section .header .angle-double-down:hover,
|
||||||
|
.dcmrt-panel-section .header .angle-double-up:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel-section .header .eye-icon.expanded {
|
||||||
|
color: var(--default-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons .angle-double-down, .icons .angle-double-up {
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './PanelSection.css';
|
||||||
|
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
|
||||||
|
|
||||||
|
const PanelSection = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
visible = false,
|
||||||
|
expanded = false,
|
||||||
|
loading = false,
|
||||||
|
hideVisibleButton = false,
|
||||||
|
onVisibilityChange = () => { },
|
||||||
|
onExpandChange = () => { }
|
||||||
|
}) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(expanded);
|
||||||
|
const [isVisible, setIsVisible] = useState(visible);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsVisible(visible);
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="dcmrt-panel-section"
|
||||||
|
style={{
|
||||||
|
marginBottom: isExpanded ? 0 : 2,
|
||||||
|
height: isExpanded ? '100%' : 'unset'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="header">
|
||||||
|
<div>{title}</div>
|
||||||
|
<div className="icons">
|
||||||
|
{!hideVisibleButton && (
|
||||||
|
<Icon
|
||||||
|
className={`eye-icon ${isVisible && 'expanded'}`}
|
||||||
|
name={isVisible ? "eye" : "eye-closed"}
|
||||||
|
width="20px"
|
||||||
|
height="20px"
|
||||||
|
onClick={() => {
|
||||||
|
const newVisibility = !isVisible;
|
||||||
|
setIsVisible(newVisibility);
|
||||||
|
onVisibilityChange(newVisibility);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Icon
|
||||||
|
className={`angle-double-${isExpanded ? 'down' : 'up'} ${isExpanded && 'expanded'}`}
|
||||||
|
name={`angle-double-${isExpanded ? 'down' : 'up'}`}
|
||||||
|
width="20px"
|
||||||
|
height="20px"
|
||||||
|
onClick={() => {
|
||||||
|
const newExpandValue = !isExpanded;
|
||||||
|
setIsExpanded(newExpandValue);
|
||||||
|
onExpandChange(newExpandValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{loading && isExpanded && <LoadingIndicator expand height="70px" width="70px" />}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PanelSection.propTypes = {
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired,
|
||||||
|
visible: PropTypes.bool,
|
||||||
|
expanded: PropTypes.bool,
|
||||||
|
onVisibilityChange: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
PanelSection.defaultProps = {
|
||||||
|
visible: false,
|
||||||
|
expanded: false,
|
||||||
|
onVisibilityChange: () => { }
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PanelSection;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
.dcmrt-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--primary-background-color);
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel .dcmrt-panel-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel .dcmrt-panel-header .cog-icon {
|
||||||
|
align-self: flex-end;
|
||||||
|
color: var(--default-color);
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-panel .dcmrt-panel-header .cog-icon:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
|
import { ScrollableArea, TableList, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
import { utils } from '@ohif/core';
|
||||||
|
|
||||||
|
import './RTPanel.css';
|
||||||
|
import StructureSetItem from '../StructureSetItem/StructureSetItem';
|
||||||
|
import RTPanelSettings from '../RTSettings/RTSettings';
|
||||||
|
import PanelSection from '../PanelSection/PanelSection';
|
||||||
|
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
|
||||||
|
|
||||||
|
const { studyMetadataManager } = utils;
|
||||||
|
|
||||||
|
const refreshViewport = () => {
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
cornerstone.updateImage(enabledElement.element);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RTPanel component
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Array} props.studies
|
||||||
|
* @param {Array} props.viewports - viewportSpecificData
|
||||||
|
* @param {number} props.activeIndex - activeViewportIndex
|
||||||
|
* @param {number} props.isOpen - isOpen
|
||||||
|
* @returns component
|
||||||
|
*/
|
||||||
|
const RTPanel = ({ studies, viewports, activeIndex, isOpen, onContourItemClick }) => {
|
||||||
|
const [selectedContour, setSelectedContour] = useState();
|
||||||
|
const DEFAULT_SET_INDEX = 0;
|
||||||
|
const DEFAULT_STATE = {
|
||||||
|
referencedDisplaysets: [],
|
||||||
|
sets: [],
|
||||||
|
selectedSet: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [state, setState] = useState(DEFAULT_STATE);
|
||||||
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
|
const activeViewport = viewports[activeIndex];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
|
||||||
|
*
|
||||||
|
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
|
||||||
|
* or other components that could rely on loaded rtstructs that
|
||||||
|
* the first batch of structs were loaded so that e.g. when the user opens the panel
|
||||||
|
* before the structs are fully loaded, the panel can subscribe to this custom event
|
||||||
|
* and update itself with the new structs.
|
||||||
|
*
|
||||||
|
* This limitation is due to the fact that the rtmodule is an object (which will be
|
||||||
|
* updated after the structs are loaded) that React its not aware of its changes
|
||||||
|
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||||
|
*
|
||||||
|
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||||
|
* allows us to easily watch the module or the rtstruct loading process in any other component
|
||||||
|
* without subscribing to external events.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('extensiondicomrtrtloaded', updateStructureSets);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateStructureSets = () => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const StructureSets = module.state.StructureSets;
|
||||||
|
|
||||||
|
if (StructureSets && StructureSets.length) {
|
||||||
|
const viewportSets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
|
||||||
|
activeViewport.SeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const studyMetadata = studyMetadataManager.get(activeViewport.StudyInstanceUID);
|
||||||
|
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||||
|
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
|
||||||
|
Modality: 'RTSTRUCT',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (viewportSets.length) {
|
||||||
|
const defaultSet = viewportSets[DEFAULT_SET_INDEX];
|
||||||
|
setState({
|
||||||
|
referencedDisplaysets,
|
||||||
|
selectedSet: defaultSet,
|
||||||
|
sets: viewportSets
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setState(DEFAULT_STATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateStructureSets();
|
||||||
|
}, [studies, viewports, activeIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowSettings(showSettings && !isOpen);
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const toContourItem = ({ ROINumber, ROIName, RTROIObservations, colorArray, visible }, loadedSet) => {
|
||||||
|
let interpretedType = '';
|
||||||
|
if (RTROIObservations && RTROIObservations.RTROIInterpretedType) {
|
||||||
|
interpretedType = `(${RTROIObservations.RTROIInterpretedType})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSameContour = selectedContour && selectedContour === ROINumber;
|
||||||
|
return (
|
||||||
|
<StructureSetItem
|
||||||
|
key={ROINumber}
|
||||||
|
selected={isSameContour}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedContour(isSameContour ? null : ROINumber);
|
||||||
|
|
||||||
|
const enabledElements = cornerstone.getEnabledElements();
|
||||||
|
const element = enabledElements[activeIndex].element;
|
||||||
|
const toolState = cornerstoneTools.getToolState(element, 'stack');
|
||||||
|
|
||||||
|
if (!toolState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIds = toolState.data[0].imageIds;
|
||||||
|
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const imageId = module.getters.imageIdOfCenterFrameOfROIContour(
|
||||||
|
state.selectedSet.SeriesInstanceUID,
|
||||||
|
ROINumber,
|
||||||
|
imageIds
|
||||||
|
);
|
||||||
|
|
||||||
|
const frameIndex = imageIds.indexOf(imageId);
|
||||||
|
const SOPInstanceUID = cornerstone.metaData.get('SOPInstanceUID', imageId);
|
||||||
|
const StudyInstanceUID = cornerstone.metaData.get('StudyInstanceUID', imageId);
|
||||||
|
|
||||||
|
onContourItemClick({
|
||||||
|
StudyInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
frameIndex,
|
||||||
|
activeViewportIndex: activeIndex
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
label={`${ROIName} ${interpretedType}`}
|
||||||
|
index={ROINumber}
|
||||||
|
color={colorArray}
|
||||||
|
visible={visible}
|
||||||
|
onVisibilityChange={() => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
module.setters.toggleROIContour(state.selectedSet.SeriesInstanceUID, ROINumber);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const configurationChangeHandler = newConfiguration => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
module.configuration.lineWidth = newConfiguration.lineWidth;
|
||||||
|
module.configuration.opacity = newConfiguration.opacity;
|
||||||
|
refreshViewport();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showSettings) {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
return (
|
||||||
|
<RTPanelSettings
|
||||||
|
configuration={module.configuration}
|
||||||
|
onBack={() => setShowSettings(false)}
|
||||||
|
onChange={configurationChangeHandler}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dcmrt-panel">
|
||||||
|
<div className="dcmrt-panel-header">
|
||||||
|
<h3>RT Structure Sets</h3>
|
||||||
|
<Icon
|
||||||
|
className="cog-icon"
|
||||||
|
name="cog"
|
||||||
|
width="25px"
|
||||||
|
height="25px"
|
||||||
|
onClick={() => setShowSettings(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!state.referencedDisplaysets.length && <LoadingIndicator expand height="70px" width="70px" />}
|
||||||
|
{state.referencedDisplaysets.map(displaySet => {
|
||||||
|
const { SeriesInstanceUID, metadata, isLoaded } = displaySet;
|
||||||
|
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||||
|
|
||||||
|
const loadedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
|
||||||
|
return (
|
||||||
|
<PanelSection
|
||||||
|
key={SeriesInstanceUID}
|
||||||
|
title={metadata.StructureSetLabel}
|
||||||
|
loading={!isLoaded || !loadedSet}
|
||||||
|
visible={isLoaded && loadedSet.visible}
|
||||||
|
hideVisibleButton={!isLoaded}
|
||||||
|
expanded={isLoaded && loadedSet.SeriesInstanceUID === state.selectedSet.SeriesInstanceUID}
|
||||||
|
onVisibilityChange={() => {
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
module.setters.toggleStructureSet(SeriesInstanceUID);
|
||||||
|
}}
|
||||||
|
onExpandChange={async () => {
|
||||||
|
if (!isLoaded) {
|
||||||
|
await displaySet.load(viewports[activeIndex], studies);
|
||||||
|
const module = cornerstoneTools.getModule('rtstruct');
|
||||||
|
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(viewports[activeIndex].SeriesInstanceUID);
|
||||||
|
const selectedSet = sets.find(set => set.SeriesInstanceUID === SeriesInstanceUID);
|
||||||
|
setState(state => ({ ...state, selectedSet, sets }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollableArea>
|
||||||
|
<TableList headless>
|
||||||
|
{isLoaded && loadedSet.ROIContours.map(c => toContourItem(c, loadedSet))}
|
||||||
|
</TableList>
|
||||||
|
</ScrollableArea>
|
||||||
|
</PanelSection>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RTPanel.propTypes = {
|
||||||
|
viewports: PropTypes.shape({
|
||||||
|
displaySetInstanceUID: PropTypes.string,
|
||||||
|
frameRate: PropTypes.any,
|
||||||
|
InstanceNumber: PropTypes.number,
|
||||||
|
isMultiFrame: PropTypes.bool,
|
||||||
|
isReconstructable: PropTypes.bool,
|
||||||
|
Modality: PropTypes.string,
|
||||||
|
plugin: PropTypes.string,
|
||||||
|
SeriesDate: PropTypes.string,
|
||||||
|
SeriesDescription: PropTypes.string,
|
||||||
|
SeriesInstanceUID: PropTypes.string,
|
||||||
|
SeriesNumber: PropTypes.any,
|
||||||
|
SeriesTime: PropTypes.string,
|
||||||
|
sopClassUIDs: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
StudyInstanceUID: PropTypes.string,
|
||||||
|
}),
|
||||||
|
activeIndex: PropTypes.number.isRequired,
|
||||||
|
studies: PropTypes.array.isRequired,
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
RTPanel.defaultProps = {};
|
||||||
|
|
||||||
|
export default RTPanel;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
.dcmrt-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-title h3 {
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-section .content {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: var(--ui-gray-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-section .header {
|
||||||
|
border-bottom: 2px solid black;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: var(--ui-gray-dark);
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .settings-section p {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range {
|
||||||
|
display: flex;
|
||||||
|
padding: 10px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range:not(:last-of-type) {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range label {
|
||||||
|
margin: 0;
|
||||||
|
display: unset;
|
||||||
|
min-width: 40px;
|
||||||
|
max-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .range input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 20px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .return-button {
|
||||||
|
color: #333;
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 4px 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-settings .return-button:hover,
|
||||||
|
.dcmrt-settings .return-button:active,
|
||||||
|
.dcmrt-settings .return-button.active {
|
||||||
|
background-color: var(--active-color);
|
||||||
|
border-color: var(--ui-border-color-active);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Range } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './RTSettings.css';
|
||||||
|
|
||||||
|
const RTSettings = ({ configuration, onBack, onChange }) => {
|
||||||
|
const toFloat = value => parseFloat(value / 100).toFixed(2);
|
||||||
|
|
||||||
|
const save = (field, value) => {
|
||||||
|
onChange({ ...configuration, [field]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsSection = ({ title, children }) => {
|
||||||
|
return (
|
||||||
|
<div className="settings-section">
|
||||||
|
<div className="header">{title}</div>
|
||||||
|
<div className="content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dcmrt-settings">
|
||||||
|
<div className="settings-title">
|
||||||
|
<h3>RT Structure Set Settings</h3>
|
||||||
|
<button className="return-button" onClick={onBack}>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingsSection title="Segment Outline">
|
||||||
|
<div className="range">
|
||||||
|
<label htmlFor="range">Opacity</label>
|
||||||
|
<Range
|
||||||
|
showPercentage
|
||||||
|
step={1}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
value={configuration.opacity * 100}
|
||||||
|
onChange={event => save('opacity', toFloat(event.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="range">
|
||||||
|
<label htmlFor="range">Width</label>
|
||||||
|
<Range
|
||||||
|
showValue
|
||||||
|
step={1}
|
||||||
|
min={1}
|
||||||
|
max={5}
|
||||||
|
value={configuration.lineWidth}
|
||||||
|
onChange={event => save('lineWidth', parseInt(event.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsSection>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RTSettings.propTypes = {
|
||||||
|
configuration: PropTypes.shape({
|
||||||
|
lineWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||||
|
opacity: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
onBack: PropTypes.func.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RTSettings;
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
.dcmrt-structure-set-item .item-color-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-color {
|
||||||
|
margin-top: 8px;
|
||||||
|
border-radius: 100%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-info {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 9px;
|
||||||
|
padding-left: 9px;
|
||||||
|
width: 90px;
|
||||||
|
border-left: 1px solid var(--text-secondary-color);
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item.selected .item-actions {
|
||||||
|
height: 35px;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions {
|
||||||
|
margin-left: -1px;
|
||||||
|
background-color: var(--ui-gray-darker);
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
visibility: hidden;
|
||||||
|
padding-left: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions .btnAction {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--default-color);
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 35px;
|
||||||
|
height: 35px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions .btnAction:hover, .dcmrt-structure-set-item .item-actions .btnAction:active {
|
||||||
|
color: var(--text-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-actions .btnAction i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item.selected .item-actions {
|
||||||
|
height: 35px;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: calc(100% - 15px); /* calc(100% - 50px); 20px = eye icon */
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label .eye-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label .eye-icon:hover {
|
||||||
|
color: var(--hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dcmrt-structure-set-item .item-label .eye-icon.--visible {
|
||||||
|
color: var(--default-color);
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { TableListItem, Icon } from '@ohif/ui';
|
||||||
|
|
||||||
|
import './StructureSetItem.css';
|
||||||
|
|
||||||
|
const ColoredCircle = ({ color }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="item-color"
|
||||||
|
style={{ backgroundColor: `rgba(${color.join(',')})` }}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ColoredCircle.propTypes = {
|
||||||
|
color: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const StructureSetItem = ({
|
||||||
|
index,
|
||||||
|
label,
|
||||||
|
onClick,
|
||||||
|
itemClass,
|
||||||
|
color,
|
||||||
|
visible = true,
|
||||||
|
onVisibilityChange,
|
||||||
|
selected = false,
|
||||||
|
}) => {
|
||||||
|
const [isVisible, setIsVisible] = useState(visible);
|
||||||
|
return (
|
||||||
|
<div className={`dcmrt-structure-set-item ${selected && 'selected'}`}>
|
||||||
|
<TableListItem
|
||||||
|
key={index}
|
||||||
|
itemKey={index}
|
||||||
|
itemIndex={index}
|
||||||
|
itemClass={itemClass}
|
||||||
|
itemMeta={<ColoredCircle color={color} />}
|
||||||
|
itemMetaClass="item-color-section"
|
||||||
|
onItemClick={onClick}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="item-label" style={{ marginBottom: 4 }}>
|
||||||
|
{label}
|
||||||
|
<Icon
|
||||||
|
className={`eye-icon ${isVisible && '--visible'}`}
|
||||||
|
name={isVisible ? 'eye' : 'eye-closed'}
|
||||||
|
width="20px"
|
||||||
|
height="20px"
|
||||||
|
onClick={event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const newVisibility = !isVisible;
|
||||||
|
setIsVisible(newVisibility);
|
||||||
|
onVisibilityChange(newVisibility);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{false && <div className="item-info">{'...'}</div>}
|
||||||
|
{false && (
|
||||||
|
<div className="item-actions">
|
||||||
|
<button
|
||||||
|
className="btnAction"
|
||||||
|
onClick={() => console.log('Relabelling...')}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: '4px' }}>
|
||||||
|
<Icon name="edit" width="14px" height="14px" />
|
||||||
|
</span>
|
||||||
|
Relabel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btnAction"
|
||||||
|
onClick={() => console.log('Editing description...')}
|
||||||
|
>
|
||||||
|
<span style={{ marginRight: '4px' }}>
|
||||||
|
<Icon name="edit" width="14px" height="14px" />
|
||||||
|
</span>
|
||||||
|
Description
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableListItem>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
StructureSetItem.propTypes = {
|
||||||
|
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
itemClass: PropTypes.string,
|
||||||
|
color: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
StructureSetItem.defaultProps = {
|
||||||
|
itemClass: '',
|
||||||
|
onClick: () => { },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StructureSetItem;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
const id = 'rt';
|
||||||
|
|
||||||
|
export default id;
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import init from './init.js';
|
||||||
|
import sopClassHandlerModule from './OHIFDicomRTStructSopClassHandler';
|
||||||
|
import id from './id.js';
|
||||||
|
import RTPanel from './components/RTPanel/RTPanel';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Only required property. Should be a unique value across all extensions.
|
||||||
|
*/
|
||||||
|
id,
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @param {object} [configuration={}]
|
||||||
|
* @param {object|array} [configuration.csToolsConfig] - Passed directly to `initCornerstoneTools`
|
||||||
|
*/
|
||||||
|
preRegistration({ servicesManager, configuration = {} }) {
|
||||||
|
init({ servicesManager, configuration });
|
||||||
|
},
|
||||||
|
getPanelModule({ commandsManager }) {
|
||||||
|
const ExtendedRTPanel = props => {
|
||||||
|
const contourItemClickHandler = contourData => {
|
||||||
|
commandsManager.runCommand('jumpToImage', contourData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RTPanel
|
||||||
|
{...props}
|
||||||
|
onContourItemClick={contourItemClickHandler}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
icon: 'list',
|
||||||
|
label: 'RTSTRUCT',
|
||||||
|
target: 'rt-panel',
|
||||||
|
isDisabled: studies => {
|
||||||
|
if (!studies) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < studies.length; i++) {
|
||||||
|
const study = studies[i];
|
||||||
|
|
||||||
|
if (study && study.series) {
|
||||||
|
for (let j = 0; j < study.series.length; j++) {
|
||||||
|
const series = study.series[j];
|
||||||
|
console.log(studies);
|
||||||
|
if (['RTSTRUCT', 'RTPLAN', 'RTDOSE'].includes(series.Modality)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
id: 'rt-panel',
|
||||||
|
component: ExtendedRTPanel,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaultContext: ['VIEWER'],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getSopClassHandlerModule({ servicesManager }) {
|
||||||
|
return sopClassHandlerModule;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import RTStructDisplayTool from './tools/RTStructDisplayTool';
|
||||||
|
import rtStructModule from './tools/modules/rtStructModule';
|
||||||
|
|
||||||
|
import TOOL_NAMES from './utils/toolNames';
|
||||||
|
|
||||||
|
const defaultConfig = {
|
||||||
|
TOOL_NAMES: {
|
||||||
|
RT_STRUCT_DISPLAY_TOOL: 'RTStructDisplayTool',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} configuration
|
||||||
|
* @param {Object|Array} configuration.csToolsConfig
|
||||||
|
*/
|
||||||
|
export default function init({ servicesManager, configuration = {} }) {
|
||||||
|
const conifg = Object.assign({}, defaultConfig, configuration);
|
||||||
|
|
||||||
|
TOOL_NAMES.RT_STRUCT_DISPLAY_TOOL = conifg.TOOL_NAMES.RT_STRUCT_DISPLAY_TOOL;
|
||||||
|
|
||||||
|
cornerstoneTools.register('module', 'rtstruct', rtStructModule);
|
||||||
|
cornerstoneTools.addTool(RTStructDisplayTool);
|
||||||
|
}
|
||||||
@@ -0,0 +1,309 @@
|
|||||||
|
import OHIF from '@ohif/core';
|
||||||
|
import * as dcmjs from 'dcmjs';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import transformPointsToImagePlane from './utils/transformPointsToImagePlane';
|
||||||
|
import TOOL_NAMES from './utils/toolNames';
|
||||||
|
|
||||||
|
const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
|
||||||
|
const globalImageIdSpecificToolStateManager =
|
||||||
|
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
const { DicomLoaderService } = OHIF.utils;
|
||||||
|
|
||||||
|
export default async function loadRTStruct(
|
||||||
|
rtStructDisplaySet,
|
||||||
|
referencedDisplaySet,
|
||||||
|
studies
|
||||||
|
) {
|
||||||
|
const rtStructModule = cornerstoneTools.getModule('rtstruct');
|
||||||
|
|
||||||
|
// Set here is loading is asynchronous.
|
||||||
|
// If this function throws its set back to false.
|
||||||
|
rtStructDisplaySet.isLoaded = true;
|
||||||
|
|
||||||
|
const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;
|
||||||
|
|
||||||
|
const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
|
||||||
|
rtStructDisplaySet,
|
||||||
|
studies
|
||||||
|
);
|
||||||
|
|
||||||
|
const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
|
||||||
|
const rtStructDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||||
|
dicomData.dict
|
||||||
|
);
|
||||||
|
|
||||||
|
rtStructDataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
|
||||||
|
dicomData.meta
|
||||||
|
);
|
||||||
|
|
||||||
|
// global cornerstone tools state to attach measurements to.
|
||||||
|
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
|
||||||
|
const {
|
||||||
|
StructureSetROISequence,
|
||||||
|
ROIContourSequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
StructureSetLabel,
|
||||||
|
} = rtStructDataset;
|
||||||
|
|
||||||
|
// Define our structure set entry and add it to the rtstruct module state.
|
||||||
|
const structureSet = {
|
||||||
|
StructureSetLabel,
|
||||||
|
SeriesInstanceUID: rtStructDataset.SeriesInstanceUID,
|
||||||
|
ROIContours: [],
|
||||||
|
referencedSeriesSequence:
|
||||||
|
rtStructDisplaySet.metadata.ReferencedSeriesSequence,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
rtStructModule.setters.structureSet(structureSet);
|
||||||
|
|
||||||
|
const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||||
|
studies,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
||||||
|
|
||||||
|
for (let i = 0; i < ROIContourSequence.length; i++) {
|
||||||
|
|
||||||
|
const ROIContour = ROIContourSequence[i];
|
||||||
|
const { ReferencedROINumber, ContourSequence } = ROIContour;
|
||||||
|
|
||||||
|
if (!ContourSequence) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setROIContourMetadata(
|
||||||
|
structureSet,
|
||||||
|
StructureSetROISequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROIContour
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let c = 0; c < ContourSequence.length; c++) {
|
||||||
|
const {
|
||||||
|
ContourImageSequence,
|
||||||
|
ContourData,
|
||||||
|
NumberOfContourPoints,
|
||||||
|
ContourGeometricType,
|
||||||
|
} = ContourSequence[c];
|
||||||
|
|
||||||
|
if (ContourGeometricType !== 'CLOSED_PLANAR') {
|
||||||
|
// TODO: Do we want to visualise types other than closed planar?
|
||||||
|
// We could easily do open planar.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sopInstanceUID = ContourImageSequence.ReferencedSOPInstanceUID;
|
||||||
|
const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);
|
||||||
|
const imageIdSpecificToolData = _getOrCreateImageIdSpecificToolData(
|
||||||
|
toolState,
|
||||||
|
imageId,
|
||||||
|
rtStructDisplayToolName
|
||||||
|
);
|
||||||
|
|
||||||
|
const imagePlane = cornerstone.metaData.get('imagePlaneModule', imageId);
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
|
||||||
|
points.push({
|
||||||
|
x: ContourData[p],
|
||||||
|
y: ContourData[p + 1],
|
||||||
|
z: ContourData[p + 2],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
transformPointsToImagePlane(points, imagePlane);
|
||||||
|
|
||||||
|
const measurementData = {
|
||||||
|
handles: {
|
||||||
|
points,
|
||||||
|
},
|
||||||
|
structureSetSeriesInstanceUid: rtStructDataset.SeriesInstanceUID,
|
||||||
|
ROINumber: ReferencedROINumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
imageIdSpecificToolData.push(measurementData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setToolEnabledIfNotEnabled(rtStructDisplayToolName);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
|
||||||
|
*
|
||||||
|
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
|
||||||
|
* or other components that could rely on loaded rtstructs that
|
||||||
|
* the first batch of structs were loaded so that e.g. when the user opens the panel
|
||||||
|
* before the structs are fully loaded, the panel can subscribe to this custom event
|
||||||
|
* and update itself with the new structs.
|
||||||
|
*
|
||||||
|
* This limitation is due to the fact that the rtmodule is an object (which will be
|
||||||
|
* updated after the structs are loaded) that React its not aware of its changes
|
||||||
|
* because the module object its not passed in to the panel component as prop but accessed externally.
|
||||||
|
*
|
||||||
|
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
|
||||||
|
* allows us to easily watch the module or the rtstruct loading process in any other component
|
||||||
|
* without subscribing to external events.
|
||||||
|
*/
|
||||||
|
const event = new CustomEvent('extensiondicomrtrtloaded');
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setROIContourMetadata(
|
||||||
|
structureSet,
|
||||||
|
StructureSetROISequence,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROIContour
|
||||||
|
) {
|
||||||
|
const StructureSetROI = StructureSetROISequence.find(
|
||||||
|
structureSetROI =>
|
||||||
|
structureSetROI.ROINumber === ROIContour.ReferencedROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
const ROIContourData = {
|
||||||
|
ROINumber: StructureSetROI.ROINumber,
|
||||||
|
ROIName: StructureSetROI.ROIName,
|
||||||
|
ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
|
||||||
|
ROIDescription: StructureSetROI.ROIDescription,
|
||||||
|
visible: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
_setROIContourDataColor(ROIContour, ROIContourData);
|
||||||
|
|
||||||
|
if (RTROIObservationsSequence) {
|
||||||
|
// If present, add additional RTROIObservations metadata.
|
||||||
|
_setROIContourRTROIObservations(
|
||||||
|
ROIContourData,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROIContour.ReferencedROINumber
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
structureSet.ROIContours.push(ROIContourData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setROIContourDataColor(ROIContour, ROIContourData) {
|
||||||
|
let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour;
|
||||||
|
|
||||||
|
if (!ROIDisplayColor && RecommendedDisplayCIELabValue) {
|
||||||
|
// If ROIDisplayColor is absent, try using the RecommendedDisplayCIELabValue color.
|
||||||
|
ROIDisplayColor = dicomlab2RGB(RecommendedDisplayCIELabValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ROIDisplayColor) {
|
||||||
|
ROIContourData.colorArray = [...ROIDisplayColor];
|
||||||
|
} else {
|
||||||
|
//Choose a color from the cornerstoneTools colorLUT
|
||||||
|
// We sample from the default color LUT here (i.e. 0), as we have nothing else to go on.
|
||||||
|
const { getters } = cornerstoneTools.getModule('segmentation');
|
||||||
|
const color = getters.colorForSegmentIndexColorLUT(
|
||||||
|
0,
|
||||||
|
ROIContourData.ROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
ROIContourData.colorArray = [...color];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setROIContourRTROIObservations(
|
||||||
|
ROIContourData,
|
||||||
|
RTROIObservationsSequence,
|
||||||
|
ROINumber
|
||||||
|
) {
|
||||||
|
const RTROIObservations = RTROIObservationsSequence.find(
|
||||||
|
RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
if (RTROIObservations) {
|
||||||
|
// Deep copy so we don't keep the reference to the dcmjs dataset entry.
|
||||||
|
const {
|
||||||
|
ObservationNumber,
|
||||||
|
ROIObservationDescription,
|
||||||
|
RTROIInterpretedType,
|
||||||
|
ROIInterpreter,
|
||||||
|
} = RTROIObservations;
|
||||||
|
|
||||||
|
ROIContourData.RTROIObservations = {
|
||||||
|
ObservationNumber,
|
||||||
|
ROIObservationDescription,
|
||||||
|
RTROIInterpretedType,
|
||||||
|
ROIInterpreter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _setToolEnabledIfNotEnabled(toolName) {
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
const { element } = enabledElement;
|
||||||
|
const tool = cornerstoneTools.getToolForElement(element, toolName);
|
||||||
|
|
||||||
|
if (tool.mode !== 'enabled') {
|
||||||
|
// If not already active or passive, set passive so contours render.
|
||||||
|
cornerstoneTools.setToolEnabled(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
cornerstone.updateImage(element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getOrCreateImageIdSpecificToolData(toolState, imageId, toolName) {
|
||||||
|
if (toolState.hasOwnProperty(imageId) === false) {
|
||||||
|
toolState[imageId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIdToolState = toolState[imageId];
|
||||||
|
|
||||||
|
// If we don't have tool state for this type of tool, add an empty object
|
||||||
|
if (imageIdToolState.hasOwnProperty(toolName) === false) {
|
||||||
|
imageIdToolState[toolName] = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageIdToolState[toolName].data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
|
||||||
|
const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
|
||||||
|
imageIdSopInstanceUidPairsEntry =>
|
||||||
|
imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
return imageIdSopInstanceUidPairsEntry.imageId;
|
||||||
|
};
|
||||||
|
|
||||||
|
function _getImageIdSopInstanceUidPairsForDisplaySet(
|
||||||
|
studies,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID
|
||||||
|
) {
|
||||||
|
const study = studies.find(
|
||||||
|
study => study.StudyInstanceUID === StudyInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
const displaySets = study.displaySets.filter(set => {
|
||||||
|
return set.SeriesInstanceUID === SeriesInstanceUID;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (displaySets.length > 1) {
|
||||||
|
console.warn(
|
||||||
|
'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
|
||||||
|
);
|
||||||
|
// TODO -> We could make check the instance list and see if any match?
|
||||||
|
// Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
|
||||||
|
// ^ Will that even happen?
|
||||||
|
}
|
||||||
|
|
||||||
|
const referencedDisplaySet = displaySets[0];
|
||||||
|
|
||||||
|
return referencedDisplaySet.images.map(image => {
|
||||||
|
return {
|
||||||
|
imageId: image.getImageId(),
|
||||||
|
sopInstanceUID: image.getSOPInstanceUID(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { importInternal, getToolState, toolColors } from 'cornerstone-tools';
|
||||||
|
|
||||||
|
import TOOL_NAMES from '../utils/toolNames';
|
||||||
|
|
||||||
|
// Cornerstone 3rd party dev kit imports
|
||||||
|
const draw = importInternal('drawing/draw');
|
||||||
|
const drawJoinedLines = importInternal('drawing/drawJoinedLines');
|
||||||
|
const getNewContext = importInternal('drawing/getNewContext');
|
||||||
|
const BaseTool = importInternal('base/BaseTool');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class RTStructDisplayTool - Renders RTSTRUCT data in a read only manner (i.e. as an overlay).
|
||||||
|
* @extends cornerstoneTools.BaseTool
|
||||||
|
*/
|
||||||
|
export default class RTStructDisplayTool extends BaseTool {
|
||||||
|
constructor(props = {}) {
|
||||||
|
const defaultProps = {
|
||||||
|
mixins: ['enabledOrDisabledBinaryTool'],
|
||||||
|
name: TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialProps = Object.assign(defaultProps, props);
|
||||||
|
|
||||||
|
super(initialProps);
|
||||||
|
|
||||||
|
this._rtStructModule = cornerstoneTools.getModule('rtstruct');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToolData(evt) {
|
||||||
|
const eventData = evt.detail;
|
||||||
|
const rtstructModule = this._rtStructModule;
|
||||||
|
|
||||||
|
const toolState = getToolState(evt.currentTarget, this.name);
|
||||||
|
|
||||||
|
if (!toolState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { lineWidth, opacity } = rtstructModule.configuration;
|
||||||
|
|
||||||
|
// We have tool data for this element - iterate over each one and draw it
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
for (let i = 0; i < toolState.data.length; i++) {
|
||||||
|
const data = toolState.data[i];
|
||||||
|
|
||||||
|
const structureSet = rtstructModule.getters.structureSet(
|
||||||
|
data.structureSetSeriesInstanceUid
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't render if entire StructureSet is hidden.
|
||||||
|
if (structureSet.visible === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROIContourData = rtstructModule.getters.ROIContour(
|
||||||
|
data.structureSetSeriesInstanceUid,
|
||||||
|
data.ROINumber
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't render if ROIContour is hidden.
|
||||||
|
if (ROIContourData.visible === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = data.handles.points;
|
||||||
|
|
||||||
|
if (!points.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colorArray = ROIContourData.colorArray;
|
||||||
|
const color = `rgba(${colorArray[0]},${colorArray[1]},${
|
||||||
|
colorArray[2]
|
||||||
|
},${opacity})`;
|
||||||
|
|
||||||
|
lineWidth;
|
||||||
|
|
||||||
|
draw(context, context => {
|
||||||
|
drawJoinedLines(
|
||||||
|
context,
|
||||||
|
eventData.element,
|
||||||
|
points[points.length - 1],
|
||||||
|
points,
|
||||||
|
{
|
||||||
|
color,
|
||||||
|
lineWidth,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import TOOL_NAMES from '../../../utils/toolNames';
|
||||||
|
|
||||||
|
const globalImageIdSpecificToolStateManager =
|
||||||
|
cornerstoneTools.globalImageIdSpecificToolStateManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getImageIdOfCenterFrameOfROIContour - Returns the imageId nearest to the center of the
|
||||||
|
* volumes extent in the stack's Z direction which contains one of the ROIContour's Contours.
|
||||||
|
* @param {string} structureSetSeriesInstanceUid The SeriesInstanceUID of the structure set.
|
||||||
|
* @param {number} ROINumber The ROINumber of the region.
|
||||||
|
* @param {string} imageIdsInStack The stack of imageIds.
|
||||||
|
*
|
||||||
|
* @returns The imageId
|
||||||
|
*/
|
||||||
|
export default function getImageIdOfCenterFrameOfROIContour(
|
||||||
|
structureSetSeriesInstanceUid,
|
||||||
|
ROINumber,
|
||||||
|
imageIdsInStack
|
||||||
|
) {
|
||||||
|
const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;
|
||||||
|
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
const imageIdIndicies = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < imageIdsInStack.length; i++) {
|
||||||
|
const imageId = imageIdsInStack[i];
|
||||||
|
|
||||||
|
const imageIdSpecificToolState = toolState[imageId];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!imageIdSpecificToolState ||
|
||||||
|
!imageIdSpecificToolState[rtStructDisplayToolName] ||
|
||||||
|
!imageIdSpecificToolState[rtStructDisplayToolName].data
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolData = imageIdSpecificToolState[rtStructDisplayToolName].data;
|
||||||
|
|
||||||
|
if (
|
||||||
|
_toolDataContainsROIContour(
|
||||||
|
toolData,
|
||||||
|
structureSetSeriesInstanceUid,
|
||||||
|
ROINumber
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
imageIdIndicies.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerImageIdIndex =
|
||||||
|
imageIdIndicies[Math.floor(imageIdIndicies.length / 2)];
|
||||||
|
|
||||||
|
return imageIdsInStack[centerImageIdIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function _toolDataContainsROIContour(
|
||||||
|
toolData,
|
||||||
|
structureSetSeriesInstanceUid,
|
||||||
|
ROINumber
|
||||||
|
) {
|
||||||
|
return !!toolData.some(
|
||||||
|
toolDataI =>
|
||||||
|
toolDataI.structureSetSeriesInstanceUid ===
|
||||||
|
structureSetSeriesInstanceUid && toolDataI.ROINumber === ROINumber
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* structureSetReferencesSeriesInstanceUid - Returns true if the structure set
|
||||||
|
* references the given SeriesInstanceUID.
|
||||||
|
* @param {*} StructureSet
|
||||||
|
* @param {*} SeriesInstanceUID
|
||||||
|
*/
|
||||||
|
export default function structureSetReferencesSeriesInstanceUid(
|
||||||
|
StructureSet,
|
||||||
|
SeriesInstanceUID
|
||||||
|
) {
|
||||||
|
const { referencedSeriesSequence } = StructureSet;
|
||||||
|
return referencedSeriesSequence.some(
|
||||||
|
referencedSeries =>
|
||||||
|
referencedSeries.SeriesInstanceUID ===
|
||||||
|
SeriesInstanceUID
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import getImageIdOfCenterFrameOfROIContour from './lib/getImageIdOfCenterFrameOfROIContour';
|
||||||
|
import structureSetReferencesSeriesInstanceUid from './lib/structureSetReferencesSeriesInstanceUid';
|
||||||
|
|
||||||
|
// We should put this as a helper somewhere as we are using it in mutliple places.
|
||||||
|
function refreshViewport() {
|
||||||
|
cornerstone.getEnabledElements().forEach(enabledElement => {
|
||||||
|
cornerstone.updateImage(enabledElement.element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuration = {
|
||||||
|
lineWidth: 3,
|
||||||
|
opacity: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
StructureSets: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a structure set to the module.
|
||||||
|
* @param {Object} structureSetData The structure set data.
|
||||||
|
*/
|
||||||
|
function setStructureSet(structureSetData) {
|
||||||
|
state.StructureSets.push(structureSetData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the StructureSet with the given SeriesInstanceUID.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*
|
||||||
|
* @returns {Object} The StructureSet.
|
||||||
|
*/
|
||||||
|
function getStructureSet(SeriesInstanceUID) {
|
||||||
|
return state.StructureSets.find(
|
||||||
|
structureSet => structureSet.SeriesInstanceUID === SeriesInstanceUID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ROI Contour with the given ROINumber on the StructureSet defined by the given
|
||||||
|
* SeriesInstanceUID.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour to fetch.
|
||||||
|
*/
|
||||||
|
function getROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
const structureSet = getStructureSet(SeriesInstanceUID);
|
||||||
|
|
||||||
|
if (!structureSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return structureSet.ROIContours.find(
|
||||||
|
ROIContour => ROIContour.ROINumber === ROINumber
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*/
|
||||||
|
function setHideStructureSet(SeriesInstanceUID) {
|
||||||
|
_setStructureSetVisible(SeriesInstanceUID, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*/
|
||||||
|
function setShowStructureSet(SeriesInstanceUID) {
|
||||||
|
_setStructureSetVisible(SeriesInstanceUID, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the visibility of the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {boolean} visible Whether the StructureSet should visible or not.
|
||||||
|
*/
|
||||||
|
function _setStructureSetVisible(SeriesInstanceUID, visible = true) {
|
||||||
|
const StructureSet = getStructureSet(SeriesInstanceUID);
|
||||||
|
|
||||||
|
if (StructureSet) {
|
||||||
|
StructureSet.visible = visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of the StructureSet.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
*/
|
||||||
|
function setToggleStructureSet(SeriesInstanceUID) {
|
||||||
|
const StructureSet = getStructureSet(SeriesInstanceUID);
|
||||||
|
|
||||||
|
if (StructureSet) {
|
||||||
|
StructureSet.visible = !StructureSet.visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
*/
|
||||||
|
function setHideROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
_setROIContourVisible(SeriesInstanceUID, ROINumber, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
*/
|
||||||
|
function setShowROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
_setROIContourVisible(SeriesInstanceUID, ROINumber, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the visibility of the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
* @param {boolean} visible Whether the StructureSet should visible or not.
|
||||||
|
*/
|
||||||
|
function _setROIContourVisible(SeriesInstanceUID, ROINumber, visible = true) {
|
||||||
|
const ROIContour = getROIContour(SeriesInstanceUID, ROINumber);
|
||||||
|
|
||||||
|
if (ROIContour) {
|
||||||
|
ROIContour.visible = visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the visibility of the ROIContour.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID of the StructureSet.
|
||||||
|
* @param {number} ROINumber The ROINUmber of the ROIContour.
|
||||||
|
*/
|
||||||
|
function setToggleROIContour(SeriesInstanceUID, ROINumber) {
|
||||||
|
const ROIContour = getROIContour(SeriesInstanceUID, ROINumber);
|
||||||
|
|
||||||
|
if (ROIContour) {
|
||||||
|
ROIContour.visible = !ROIContour.visible;
|
||||||
|
|
||||||
|
refreshViewport();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of StructureSets which reference the given SeriesInstanceUID.
|
||||||
|
* @param {string} SeriesInstanceUID The SeriesInstanceUID to check.
|
||||||
|
*/
|
||||||
|
function getStructuresSetsWhichReferenceSeriesInstanceUid(
|
||||||
|
SeriesInstanceUID
|
||||||
|
) {
|
||||||
|
const { StructureSets } = state;
|
||||||
|
return StructureSets.filter(StructureSet =>
|
||||||
|
structureSetReferencesSeriesInstanceUid(
|
||||||
|
StructureSet,
|
||||||
|
SeriesInstanceUID
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getters: {
|
||||||
|
structureSet: getStructureSet,
|
||||||
|
ROIContour: getROIContour,
|
||||||
|
structuresSetsWhichReferenceSeriesInstanceUid: getStructuresSetsWhichReferenceSeriesInstanceUid,
|
||||||
|
imageIdOfCenterFrameOfROIContour: getImageIdOfCenterFrameOfROIContour,
|
||||||
|
},
|
||||||
|
setters: {
|
||||||
|
structureSet: setStructureSet,
|
||||||
|
hideROIContour: setHideROIContour,
|
||||||
|
showROIContour: setShowROIContour,
|
||||||
|
toggleROIContour: setToggleROIContour,
|
||||||
|
hideStructureSet: setHideStructureSet,
|
||||||
|
showStructureSet: setShowStructureSet,
|
||||||
|
toggleStructureSet: setToggleStructureSet,
|
||||||
|
},
|
||||||
|
state,
|
||||||
|
configuration,
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const TOOL_NAMES = {
|
||||||
|
RTSTRUCT_DISPLAY_TOOL: 'RTStructDisplayTool',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TOOL_NAMES;
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
export default function transformPointsToImagePlane(points, imagePlane) {
|
||||||
|
// See Equation C.7.6.2.1-1 of the DICOM standard
|
||||||
|
|
||||||
|
const {
|
||||||
|
rowCosines,
|
||||||
|
columnCosines,
|
||||||
|
rowPixelSpacing: deltaI,
|
||||||
|
columnPixelSpacing: deltaJ,
|
||||||
|
imagePositionPatient,
|
||||||
|
} = imagePlane;
|
||||||
|
|
||||||
|
const X = [rowCosines[0], rowCosines[1], rowCosines[2]];
|
||||||
|
const Y = [columnCosines[0], columnCosines[1], columnCosines[2]];
|
||||||
|
const S = [
|
||||||
|
imagePositionPatient[0],
|
||||||
|
imagePositionPatient[1],
|
||||||
|
imagePositionPatient[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
// 9 sets of simulataneous equations to choose from, choose which set to solve
|
||||||
|
// Based on the largest component of each direction cosine.
|
||||||
|
// This avoids NaNs or floating point errors caused by dividing by very small numbers and ensures a safe mapping
|
||||||
|
// when mapping between planes that are close to orthogonal.
|
||||||
|
|
||||||
|
let ix = 0;
|
||||||
|
let iy = 0;
|
||||||
|
let largestDirectionCosineMagnitude = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find the element with the largest magnitude in each direction cosine vector.
|
||||||
|
for (let i = 0; i < X.length; i++) {
|
||||||
|
if (Math.abs(X[i]) > largestDirectionCosineMagnitude.x) {
|
||||||
|
ix = i;
|
||||||
|
largestDirectionCosineMagnitude.x = Math.abs(X[i]);
|
||||||
|
}
|
||||||
|
if (Math.abs(Y[i]) > largestDirectionCosineMagnitude.y) {
|
||||||
|
iy = i;
|
||||||
|
largestDirectionCosineMagnitude.y = Math.abs(Y[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ci = {
|
||||||
|
// Index of max elements in X and Y
|
||||||
|
ix,
|
||||||
|
iy,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sanity Check
|
||||||
|
const directionCosineMagnitude = {
|
||||||
|
x: Math.pow(X[0], 2) + Math.pow(X[1], 2) + Math.pow(X[2], 2),
|
||||||
|
y: Math.pow(Y[0], 2) + Math.pow(Y[1], 2) + Math.pow(Y[2], 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (directionCosineMagnitude.x < 0.99 || directionCosineMagnitude.y < 0.99) {
|
||||||
|
throw Error(
|
||||||
|
`Direction cosines do not sum to 1 in quadrature. There is likely a mistake in the DICOM metadata.` +
|
||||||
|
`directionCosineMagnitudes: ${directionCosineMagnitude.x}, ${directionCosineMagnitude.y}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in elements that won't change between points
|
||||||
|
const c = [undefined, Y[ci.ix], X[ci.ix], undefined, X[ci.iy], Y[ci.iy]];
|
||||||
|
|
||||||
|
for (let pointI = 0; pointI < points.length; pointI++) {
|
||||||
|
// Subtract imagePositionPatient from the coordinate
|
||||||
|
const r = [
|
||||||
|
points[pointI].x - S[0],
|
||||||
|
points[pointI].y - S[1],
|
||||||
|
points[pointI].z - S[2],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Set the variable terms in c.
|
||||||
|
c[0] = r[ci.ix];
|
||||||
|
c[3] = r[ci.iy];
|
||||||
|
|
||||||
|
// General case: Solves the two choosen simulataneous equations to go from the patient coordinate system to the imagePlane.
|
||||||
|
const i =
|
||||||
|
(c[0] - (c[1] * c[3]) / c[5]) /
|
||||||
|
(c[2] * deltaI * (1 - (c[1] * c[4]) / (c[2] * c[5])));
|
||||||
|
const j = (c[3] - c[4] * i * deltaI) / (c[5] * deltaJ);
|
||||||
|
|
||||||
|
// NOTE: Add (0.5, 0.5) to the coordinate, as PCS reference frame is with respect to the centre of the first pixel.
|
||||||
|
points[pointI].x = i + 0.5;
|
||||||
|
points[pointI].y = j + 0.5;
|
||||||
|
delete points[pointI].z;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [0.1.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.1...@ohif/extension-dicom-segmentation@0.1.2) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.0...@ohif/extension-dicom-segmentation@0.1.1) (2020-04-01)
|
## [0.1.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.1.0...@ohif/extension-dicom-segmentation@0.1.1) (2020-04-01)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-segmentation",
|
"name": "@ohif/extension-dicom-segmentation",
|
||||||
"version": "0.1.1",
|
"version": "0.1.2",
|
||||||
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
"description": "OHIF extension for viewing segmentations in the 2D MPR view",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ SegmentationPanel.propTypes = {
|
|||||||
}),
|
}),
|
||||||
activeIndex: PropTypes.number.isRequired,
|
activeIndex: PropTypes.number.isRequired,
|
||||||
studies: PropTypes.array.isRequired,
|
studies: PropTypes.array.isRequired,
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
SegmentationPanel.defaultProps = {};
|
SegmentationPanel.defaultProps = {};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,35 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.3.10](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.9...@ohif/extension-vtk@1.3.10) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See
|
||||||
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.3.9](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.8...@ohif/extension-vtk@1.3.9) (2020-04-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.7...@ohif/extension-vtk@1.3.8) (2020-04-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.3.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.6...@ohif/extension-vtk@1.3.7) (2020-04-02)
|
## [1.3.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.3.6...@ohif/extension-vtk@1.3.7) (2020-04-02)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-vtk
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-vtk",
|
"name": "@ohif/extension-vtk",
|
||||||
"version": "1.3.7",
|
"version": "1.3.10",
|
||||||
"description": "OHIF extension for VTK.js",
|
"description": "OHIF extension for VTK.js",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
"react-vtkjs-viewport": "^0.8.3"
|
"react-vtkjs-viewport": "^0.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ohif/core": "^2.6.6",
|
"@ohif/core": "^2.6.9",
|
||||||
"@ohif/ui": "^1.4.1",
|
"@ohif/ui": "^1.4.3",
|
||||||
"cornerstone-tools": "4.12.5",
|
"cornerstone-tools": "4.12.5",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
|
|||||||
@@ -3,6 +3,38 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.6.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.8...@ohif/core@2.6.9) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See
|
||||||
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [2.6.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.7...@ohif/core@2.6.8) (2020-04-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.6.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.6...@ohif/core@2.6.7) (2020-04-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* 🐛 Fix multiframe images ([#1595](https://github.com/OHIF/Viewers/issues/1595)) ([9e0bd52](https://github.com/OHIF/Viewers/commit/9e0bd52c6a86648eb6673344a8555ad787043e5c))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.6.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.5...@ohif/core@2.6.6) (2020-04-02)
|
## [2.6.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.6.5...@ohif/core@2.6.6) (2020-04-02)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/core
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/core",
|
"name": "@ohif/core",
|
||||||
"version": "2.6.6",
|
"version": "2.6.9",
|
||||||
"description": "Generic business logic for web-based medical imaging applications",
|
"description": "Generic business logic for web-based medical imaging applications",
|
||||||
"author": "OHIF Core Team",
|
"author": "OHIF Core Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"ajv": "^6.10.0",
|
"ajv": "^6.10.0",
|
||||||
"dcmjs": "^0.8.3",
|
"dcmjs": "^0.12.0",
|
||||||
"dicomweb-client": "^0.5.2",
|
"dicomweb-client": "^0.5.2",
|
||||||
"immer": "6.0.2",
|
"immer": "6.0.2",
|
||||||
"isomorphic-base64": "^1.0.2",
|
"isomorphic-base64": "^1.0.2",
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ const findMostRecentStructuredReport = studies => {
|
|||||||
studies.forEach(study => {
|
studies.forEach(study => {
|
||||||
const allSeries = study.getSeries ? study.getSeries() : [];
|
const allSeries = study.getSeries ? study.getSeries() : [];
|
||||||
allSeries.forEach(series => {
|
allSeries.forEach(series => {
|
||||||
|
// Skip series that may not have instances yet
|
||||||
|
// This can happen if we have retrieved just the initial
|
||||||
|
// details about the series via QIDO-RS, but not the full metadata
|
||||||
|
if (!series.instances.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isStructuredReportSeries(series)) {
|
if (isStructuredReportSeries(series)) {
|
||||||
if (
|
if (
|
||||||
!mostRecentStructuredReport ||
|
!mostRecentStructuredReport ||
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export class StudyMetadata extends Metadata {
|
|||||||
Object.defineProperty(this, 'studyInstanceUID', {
|
Object.defineProperty(this, 'studyInstanceUID', {
|
||||||
configurable: false,
|
configurable: false,
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
get: function () {
|
get: function() {
|
||||||
return this.getStudyInstanceUID();
|
return this.getStudyInstanceUID();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -100,20 +100,32 @@ export class StudyMetadata extends Metadata {
|
|||||||
* Split a series metadata object into display sets
|
* Split a series metadata object into display sets
|
||||||
* @param {Array} sopClassHandlerModules List of SOP Class Modules
|
* @param {Array} sopClassHandlerModules List of SOP Class Modules
|
||||||
* @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
|
||||||
* @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
|
* @returns {Array} The list of display sets created for the given series object
|
||||||
*/
|
*/
|
||||||
_createDisplaySetsForSeries(
|
_createDisplaySetsForSeries(
|
||||||
sopClassHandlerModules,
|
sopClassHandlerModules,
|
||||||
series,
|
series,
|
||||||
givenDisplaySets
|
|
||||||
) {
|
) {
|
||||||
const study = this;
|
const study = this;
|
||||||
const displaySets = Array.isArray(givenDisplaySets) ? givenDisplaySets : [];
|
const displaySets = [];
|
||||||
|
|
||||||
const anyInstances = series.getInstanceCount() > 0;
|
const anyInstances = series.getInstanceCount() > 0;
|
||||||
|
|
||||||
if (!anyInstances) {
|
if (!anyInstances) {
|
||||||
return;
|
const displaySet = new ImageSet([]);
|
||||||
|
const seriesData = series.getData();
|
||||||
|
|
||||||
|
displaySet.setAttributes({
|
||||||
|
displaySetInstanceUID: displaySet.uid,
|
||||||
|
SeriesInstanceUID: seriesData.SeriesInstanceUID,
|
||||||
|
SeriesDescription: seriesData.SeriesDescription,
|
||||||
|
SeriesNumber: seriesData.SeriesNumber,
|
||||||
|
Modality: seriesData.Modality,
|
||||||
|
});
|
||||||
|
|
||||||
|
displaySets.push(displaySet);
|
||||||
|
|
||||||
|
return displaySets;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sopClassUIDs = getSopClassUIDs(series);
|
const sopClassUIDs = getSopClassUIDs(series);
|
||||||
@@ -282,13 +294,14 @@ export class StudyMetadata extends Metadata {
|
|||||||
|
|
||||||
// Loop through the series (SeriesMetadata)
|
// Loop through the series (SeriesMetadata)
|
||||||
this.forEachSeries(
|
this.forEachSeries(
|
||||||
series =>
|
series => {
|
||||||
void this._createDisplaySetsForSeries(
|
const displaySetsForSeries = this._createDisplaySetsForSeries(
|
||||||
sopClassHandlerModules,
|
sopClassHandlerModules,
|
||||||
series,
|
series,
|
||||||
displaySets
|
);
|
||||||
)
|
|
||||||
);
|
displaySets.push(...displaySetsForSeries);
|
||||||
|
});
|
||||||
|
|
||||||
return sortDisplaySetList(displaySets);
|
return sortDisplaySetList(displaySets);
|
||||||
}
|
}
|
||||||
@@ -304,13 +317,27 @@ export class StudyMetadata extends Metadata {
|
|||||||
* @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
|
* @returns {boolean} Returns true on success or false on failure (e.g., the series does not belong to this study)
|
||||||
*/
|
*/
|
||||||
createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
|
createAndAddDisplaySetsForSeries(sopClassHandlerModules, series) {
|
||||||
if (this.containsSeries(series)) {
|
if (!this.containsSeries(series)) {
|
||||||
this.setDisplaySets(
|
return false;
|
||||||
this._createDisplaySetsForSeries(sopClassHandlerModules, series)
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
const displaySets = this._createDisplaySetsForSeries(sopClassHandlerModules, series)
|
||||||
|
|
||||||
|
// Note: filtering in place because this._displaySets has writable: false
|
||||||
|
for (let i = this._displaySets.length - 1; i >= 0; i--) {
|
||||||
|
const displaySet = this._displaySets[i];
|
||||||
|
if (displaySet.SeriesInstanceUID === series.getSeriesInstanceUID()) {
|
||||||
|
this._displaySets.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displaySets.forEach(displaySet => {
|
||||||
|
this.addDisplaySet(displaySet);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sortDisplaySets();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -319,6 +346,9 @@ export class StudyMetadata extends Metadata {
|
|||||||
*/
|
*/
|
||||||
setDisplaySets(displaySets) {
|
setDisplaySets(displaySets) {
|
||||||
if (Array.isArray(displaySets) && displaySets.length > 0) {
|
if (Array.isArray(displaySets) && displaySets.length > 0) {
|
||||||
|
// TODO: This is weird, can we just switch it to writable: true?
|
||||||
|
this._displaySets.splice(0);
|
||||||
|
|
||||||
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
displaySets.forEach(displaySet => this.addDisplaySet(displaySet));
|
||||||
this.sortDisplaySets();
|
this.sortDisplaySets();
|
||||||
}
|
}
|
||||||
@@ -407,6 +437,30 @@ export class StudyMetadata extends Metadata {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a series in the current study by SeriesInstanceUID.
|
||||||
|
* @param {String} SeriesInstanceUID The SeriesInstanceUID to be updated
|
||||||
|
* @param {SeriesMetadata} series The series to be added to the current study.
|
||||||
|
* @returns {boolean} Returns true on success, false otherwise.
|
||||||
|
*/
|
||||||
|
updateSeries(SeriesInstanceUID, series) {
|
||||||
|
const index = this._series.findIndex(series => {
|
||||||
|
return series.getSeriesInstanceUID() === SeriesInstanceUID;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(series instanceof SeriesMetadata)) {
|
||||||
|
throw new Error('Series must be an instance of SeriesMetadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._series[index] = series;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a series by index.
|
* Find a series by index.
|
||||||
* @param {number} index An integer representing a list index.
|
* @param {number} index An integer representing a list index.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { api } from 'dicomweb-client';
|
import { api } from 'dicomweb-client';
|
||||||
|
import dcmjs from 'dcmjs';
|
||||||
import DICOMWeb from '../../../DICOMWeb/';
|
import DICOMWeb from '../../../DICOMWeb/';
|
||||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||||
import { sortStudySeries, sortingCriteria } from '../../sortStudy';
|
import { sortStudySeries, sortingCriteria } from '../../sortStudy';
|
||||||
@@ -8,6 +9,8 @@ import {
|
|||||||
addInstancesToStudy,
|
addInstancesToStudy,
|
||||||
} from './studyInstanceHelpers';
|
} from './studyInstanceHelpers';
|
||||||
|
|
||||||
|
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map series to an array of SeriesInstanceUID
|
* Map series to an array of SeriesInstanceUID
|
||||||
* @param {Arrays} series list of Series Instance UIDs
|
* @param {Arrays} series list of Series Instance UIDs
|
||||||
@@ -101,15 +104,21 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
|||||||
|
|
||||||
async preLoad() {
|
async preLoad() {
|
||||||
const preLoaders = this.getPreLoaders();
|
const preLoaders = this.getPreLoaders();
|
||||||
const result = await this.runLoaders(preLoaders);
|
|
||||||
|
// seriesData is the result of the QIDO-RS Search For Series request
|
||||||
|
// It's an array of Objects containing DICOM Tag values at the Series level
|
||||||
|
const seriesData = await this.runLoaders(preLoaders);
|
||||||
|
|
||||||
const seriesSorted = sortStudySeries(
|
const seriesSorted = sortStudySeries(
|
||||||
result,
|
seriesData,
|
||||||
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria
|
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria
|
||||||
);
|
);
|
||||||
const seriesInstanceUIDsMap = mapStudySeries(seriesSorted);
|
const seriesInstanceUIDsMap = mapStudySeries(seriesSorted);
|
||||||
|
|
||||||
return seriesInstanceUIDsMap;
|
return {
|
||||||
|
seriesInstanceUIDsMap,
|
||||||
|
seriesData
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(preLoadData) {
|
async load(preLoadData) {
|
||||||
@@ -118,7 +127,7 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
|||||||
const seriesAsyncLoader = makeSeriesAsyncLoader(
|
const seriesAsyncLoader = makeSeriesAsyncLoader(
|
||||||
client,
|
client,
|
||||||
studyInstanceUID,
|
studyInstanceUID,
|
||||||
preLoadData
|
preLoadData.seriesInstanceUIDsMap
|
||||||
);
|
);
|
||||||
|
|
||||||
const firstSeries = await seriesAsyncLoader.next();
|
const firstSeries = await seriesAsyncLoader.next();
|
||||||
@@ -126,16 +135,38 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
|||||||
return {
|
return {
|
||||||
sopInstances: firstSeries.sopInstances,
|
sopInstances: firstSeries.sopInstances,
|
||||||
asyncLoader: seriesAsyncLoader,
|
asyncLoader: seriesAsyncLoader,
|
||||||
|
seriesData: preLoadData.seriesData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async posLoad(loadData) {
|
async posLoad(loadData) {
|
||||||
const { server } = this;
|
const { server } = this;
|
||||||
|
|
||||||
const { sopInstances, asyncLoader } = loadData;
|
const { sopInstances, asyncLoader, seriesData } = loadData;
|
||||||
|
|
||||||
const study = await createStudyFromSOPInstanceList(server, sopInstances);
|
const study = await createStudyFromSOPInstanceList(server, sopInstances);
|
||||||
|
|
||||||
|
// TODO: Should this be in a helper
|
||||||
|
const seriesDataNaturalized = seriesData.map(naturalizeDataset);
|
||||||
|
|
||||||
|
seriesDataNaturalized.forEach((series, idx) => {
|
||||||
|
const seriesDataFromQIDO = {
|
||||||
|
SeriesInstanceUID: series.SeriesInstanceUID,
|
||||||
|
SeriesDescription: series.SeriesDescription,
|
||||||
|
SeriesNumber: series.SeriesNumber,
|
||||||
|
Modality: series.Modality,
|
||||||
|
instances: []
|
||||||
|
};
|
||||||
|
|
||||||
|
if (study.series[idx]) {
|
||||||
|
study.series[idx] = Object.assign(seriesDataFromQIDO, study.series[idx]);
|
||||||
|
} else {
|
||||||
|
study.series[idx] = seriesDataFromQIDO;
|
||||||
|
}
|
||||||
|
|
||||||
|
study.seriesMap[series.SeriesInstanceUID] = study.series[idx];
|
||||||
|
});
|
||||||
|
|
||||||
if (asyncLoader.hasNext()) {
|
if (asyncLoader.hasNext()) {
|
||||||
attachSeriesLoader(server, study, asyncLoader);
|
attachSeriesLoader(server, study, asyncLoader);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ async function makeSOPInstance(server, study, instance) {
|
|||||||
wadorsuri,
|
wadorsuri,
|
||||||
wadoRoot: server.wadoRoot,
|
wadoRoot: server.wadoRoot,
|
||||||
imageRendering: server.imageRendering,
|
imageRendering: server.imageRendering,
|
||||||
thumbnailRendering: server.thumbnailRendering,
|
thumbnailRendering: server.thumbnailRendering
|
||||||
};
|
};
|
||||||
|
|
||||||
series.instances.push(sopInstance);
|
series.instances.push(sopInstance);
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ function createAndAddStack(stackMap, study, displaySet, stackUpdatedCallbacks) {
|
|||||||
imageIndex: imageIndex + 1,
|
imageIndex: imageIndex + 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const NumberOfFrames = image.NumberOfFrames;
|
const naturalizedInstance = instance.getData().metadata;
|
||||||
|
const NumberOfFrames = naturalizedInstance.NumberOfFrames;
|
||||||
|
|
||||||
if (NumberOfFrames > 1) {
|
if (NumberOfFrames > 1) {
|
||||||
for (let i = 0; i < NumberOfFrames; i++) {
|
for (let i = 0; i < NumberOfFrames; i++) {
|
||||||
@@ -60,8 +61,6 @@ function createAndAddStack(stackMap, study, displaySet, stackUpdatedCallbacks) {
|
|||||||
imageId = getImageId(image);
|
imageId = getImageId(image);
|
||||||
imageIds.push(imageId);
|
imageIds.push(imageId);
|
||||||
|
|
||||||
const naturalizedInstance = instance.getData().metadata;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
.locize
|
||||||
@@ -3,6 +3,14 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [0.52.8](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.7...@ohif/i18n@0.52.8) (2020-04-07)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/i18n
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.52.7](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.6...@ohif/i18n@0.52.7) (2020-03-09)
|
## [0.52.7](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.6...@ohif/i18n@0.52.7) (2020-03-09)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/i18n
|
**Note:** Version bump only for package @ohif/i18n
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/i18n",
|
"name": "@ohif/i18n",
|
||||||
"version": "0.52.7",
|
"version": "0.52.8",
|
||||||
"description": "Internationalization library for The OHIF Viewer",
|
"description": "Internationalization library for The OHIF Viewer",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'ar': {
|
'ar': {
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -13,4 +13,4 @@
|
|||||||
"Show": "Show",
|
"Show": "Show",
|
||||||
"Stop": "Stop",
|
"Stop": "Stop",
|
||||||
"StudyDate": "Study Date"
|
"StudyDate": "Study Date"
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ import StudyList from './StudyList.json';
|
|||||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'en-US': {
|
'en-US': {
|
||||||
AboutModal,
|
AboutModal,
|
||||||
Buttons,
|
Buttons,
|
||||||
@@ -21,5 +21,5 @@ export default {
|
|||||||
StudyList,
|
StudyList,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
ViewportDownloadForm,
|
ViewportDownloadForm,
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import Buttons from "./Buttons.json";
|
import Buttons from './Buttons.json';
|
||||||
import CineDialog from "./CineDialog.json";
|
import CineDialog from './CineDialog.json';
|
||||||
import Common from "./Common.json";
|
import Common from './Common.json';
|
||||||
import Header from "./Header.json";
|
import Header from './Header.json';
|
||||||
import MeasurementTable from "./MeasurementTable.json";
|
import MeasurementTable from './MeasurementTable.json';
|
||||||
import StudyList from "./StudyList.json";
|
import StudyList from './StudyList.json';
|
||||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'es': {
|
'es': {
|
||||||
@@ -16,4 +16,4 @@ export default {
|
|||||||
StudyList,
|
StudyList,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"Acquired": "Acquis",
|
||||||
|
"Angle": "Angle",
|
||||||
|
"Axial": "Axial",
|
||||||
|
"Bidirectional": "Bi-directionel",
|
||||||
|
"Brush": "Brosse",
|
||||||
|
"CINE": "Ciné",
|
||||||
|
"Cancel": "Annuler",
|
||||||
|
"Circle": "Cercle",
|
||||||
|
"Clear": "Effacer",
|
||||||
|
"Coronal": "Coronal",
|
||||||
|
"Crosshairs": "Repère",
|
||||||
|
"Ellipse": "Ellipse",
|
||||||
|
"Elliptical": "Elliptique",
|
||||||
|
"Flip H": "Flip H",
|
||||||
|
"Flip V": "Flip V",
|
||||||
|
"Freehand": "Main levée",
|
||||||
|
"Invert": "Inverser",
|
||||||
|
"Layout": "$t(Common:Layout)",
|
||||||
|
"Length": "Longueur",
|
||||||
|
"Levels": "Niveaux",
|
||||||
|
"Magnify": "Agrandir",
|
||||||
|
"Manual": "Manuel",
|
||||||
|
"Measurements": "Mesures",
|
||||||
|
"More": "$t(Common:More)",
|
||||||
|
"Next": "$t(Common:Next)",
|
||||||
|
"Pan": "Déplacer",
|
||||||
|
"Play": "$t(Common:Play)",
|
||||||
|
"Previous": "$t(Common:Previous)",
|
||||||
|
"Probe": "Sonde",
|
||||||
|
"ROI Window": "ROI fenêtrage",
|
||||||
|
"Rectangle": "Rectangle",
|
||||||
|
"Reset": "$t(Common:Reset)",
|
||||||
|
"Reset to Defaults": "Valeurs d'usine",
|
||||||
|
"Rotate Right": "Tourner à droite",
|
||||||
|
"Sagittal": "Sagittal",
|
||||||
|
"Save": "Sauvegarder",
|
||||||
|
"Stack Scroll": "Défilement",
|
||||||
|
"Stop": "$t(Common:Stop)",
|
||||||
|
"Themes": "Themes",
|
||||||
|
"Zoom": "Zoom"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Next image": "$t(Common:Play) $t(Common:Image)",
|
||||||
|
"Play / Stop": "$t(Common:Play) / $t(Common:Stop)",
|
||||||
|
"Previous image": "$t(Common:Previous) $t(Common:Image)",
|
||||||
|
"Skip to first image": "Retour à la première $t(Common:Image)",
|
||||||
|
"Skip to last image": "Aller à la dernière $t(Common:Image)",
|
||||||
|
"fps": "ips"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"Image": "Image",
|
||||||
|
"Layout": "Disposition",
|
||||||
|
"More": "Plus",
|
||||||
|
"Next": "Suivant",
|
||||||
|
"Play": "Play",
|
||||||
|
"Previous": "Précédent",
|
||||||
|
"Reset": "Reset",
|
||||||
|
"Stop": "Stop"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"About": "A Propos",
|
||||||
|
"Back to Viewer": "Retour au viewer",
|
||||||
|
"INVESTIGATIONAL USE ONLY": "Seulement pour utilisation expérimentale",
|
||||||
|
"Options": "Options",
|
||||||
|
"Preferences": "Préférences",
|
||||||
|
"Study list": "Liste d'études"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"Cancel": "$t(Buttons:Cancel)",
|
||||||
|
"Reset to Defaults": "$t(Buttons:Reset to Defaults)",
|
||||||
|
"Save": "$t(Buttons:Save)",
|
||||||
|
"User Preferences": "Préférences utilisateur"
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import Buttons from './Buttons.json';
|
||||||
|
import CineDialog from './CineDialog.json';
|
||||||
|
import Common from './Common.json';
|
||||||
|
import Header from './Header.json';
|
||||||
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'fr': {
|
||||||
|
Buttons,
|
||||||
|
CineDialog,
|
||||||
|
Common,
|
||||||
|
Header,
|
||||||
|
UserPreferencesModal,
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import ar from './ar/';
|
import ar from './ar/';
|
||||||
import en_US from './en-US/';
|
import en_US from './en-US/';
|
||||||
import es from './es/';
|
import es from './es/';
|
||||||
|
import fr from './fr/';
|
||||||
import ja_JP from './ja-JP/';
|
import ja_JP from './ja-JP/';
|
||||||
import nl from './nl/';
|
import nl from './nl/';
|
||||||
import pt_BR from './pt-BR/';
|
import pt_BR from './pt-BR/';
|
||||||
@@ -11,9 +12,10 @@ export default {
|
|||||||
...ar,
|
...ar,
|
||||||
...en_US,
|
...en_US,
|
||||||
...es,
|
...es,
|
||||||
|
...fr,
|
||||||
...ja_JP,
|
...ja_JP,
|
||||||
...nl,
|
...nl,
|
||||||
...pt_BR,
|
...pt_BR,
|
||||||
...vi,
|
...vi,
|
||||||
...zh,
|
...zh,
|
||||||
};
|
};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import Buttons from "./Buttons.json";
|
import Buttons from './Buttons.json';
|
||||||
import CineDialog from "./CineDialog.json";
|
import CineDialog from './CineDialog.json';
|
||||||
import Common from "./Common.json";
|
import Common from './Common.json';
|
||||||
import Header from "./Header.json";
|
import Header from './Header.json';
|
||||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'ja-JP': {
|
'ja-JP': {
|
||||||
@@ -12,4 +12,4 @@ export default {
|
|||||||
Header,
|
Header,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import Buttons from "./Buttons.json";
|
import Buttons from './Buttons.json';
|
||||||
import Common from "./Common.json";
|
import Common from './Common.json';
|
||||||
import Header from "./Header.json";
|
import Header from './Header.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'nl': {
|
'nl': {
|
||||||
@@ -8,4 +8,4 @@ export default {
|
|||||||
Common,
|
Common,
|
||||||
Header,
|
Header,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -6,7 +6,7 @@ import DatePicker from './DatePicker.json';
|
|||||||
import Header from './Header.json';
|
import Header from './Header.json';
|
||||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'pt-BR': {
|
'pt-BR': {
|
||||||
AboutModal,
|
AboutModal,
|
||||||
Buttons,
|
Buttons,
|
||||||
@@ -15,5 +15,5 @@ export default {
|
|||||||
DatePicker,
|
DatePicker,
|
||||||
Header,
|
Header,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import Buttons from "./Buttons.json";
|
import Buttons from './Buttons.json';
|
||||||
import CineDialog from "./CineDialog.json";
|
import CineDialog from './CineDialog.json';
|
||||||
import Common from "./Common.json";
|
import Common from './Common.json';
|
||||||
import Header from "./Header.json";
|
import Header from './Header.json';
|
||||||
import StudyList from "./StudyList.json";
|
import StudyList from './StudyList.json';
|
||||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'vi': {
|
'vi': {
|
||||||
@@ -14,4 +14,4 @@ export default {
|
|||||||
StudyList,
|
StudyList,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import Buttons from "./Buttons.json";
|
import Buttons from './Buttons.json';
|
||||||
import CineDialog from "./CineDialog.json";
|
import CineDialog from './CineDialog.json';
|
||||||
import Common from "./Common.json";
|
import Common from './Common.json';
|
||||||
import Header from "./Header.json";
|
import Header from './Header.json';
|
||||||
import MeasurementTable from "./MeasurementTable.json";
|
import MeasurementTable from './MeasurementTable.json';
|
||||||
import StudyList from "./StudyList.json";
|
import StudyList from './StudyList.json';
|
||||||
import UserPreferencesModal from "./UserPreferencesModal.json";
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'zh': {
|
'zh': {
|
||||||
@@ -16,4 +16,4 @@ export default {
|
|||||||
StudyList,
|
StudyList,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -43,7 +43,7 @@ directories.forEach(directory => {
|
|||||||
const filename = path.basename(file);
|
const filename = path.basename(file);
|
||||||
const namespace = path.basename(file, '.json');
|
const namespace = path.basename(file, '.json');
|
||||||
|
|
||||||
content += `import ${namespace} from "./${filename}";\n`;
|
content += `import ${namespace} from './${filename}';\n`;
|
||||||
});
|
});
|
||||||
|
|
||||||
content += '\n';
|
content += '\n';
|
||||||
@@ -51,7 +51,7 @@ directories.forEach(directory => {
|
|||||||
namespaces.forEach(namespace => {
|
namespaces.forEach(namespace => {
|
||||||
exportLines += ` ${namespace},\n`;
|
exportLines += ` ${namespace},\n`;
|
||||||
});
|
});
|
||||||
exportLines += ' }\n};';
|
exportLines += ' }\n};\n';
|
||||||
|
|
||||||
content += exportLines;
|
content += exportLines;
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ languageVariables.forEach(language => {
|
|||||||
fileContent += ` ...${language},\n`;
|
fileContent += ` ...${language},\n`;
|
||||||
});
|
});
|
||||||
|
|
||||||
fileContent += '};';
|
fileContent += '};\n';
|
||||||
|
|
||||||
// Create one file (index.js) inside the locales folder
|
// Create one file (index.js) inside the locales folder
|
||||||
// which exports each of the languages
|
// which exports each of the languages
|
||||||
|
|||||||
@@ -3,6 +3,27 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.4.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.2...@ohif/ui@1.4.3) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/ui
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See
|
||||||
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.4.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.1...@ohif/ui@1.4.2) (2020-04-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/ui
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.0...@ohif/ui@1.4.1) (2020-03-17)
|
## [1.4.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.4.0...@ohif/ui@1.4.1) (2020-03-17)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/ui",
|
"name": "@ohif/ui",
|
||||||
"version": "1.4.1",
|
"version": "1.4.3",
|
||||||
"description": "A set of React components for Medical Imaging Viewers",
|
"description": "A set of React components for Medical Imaging Viewers",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -9,14 +9,21 @@ export class TableList extends Component {
|
|||||||
defaultItems: PropTypes.object,
|
defaultItems: PropTypes.object,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
headerTitle: PropTypes.string,
|
headerTitle: PropTypes.string,
|
||||||
|
headless: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
headless: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="tableList">
|
<div className="tableList">
|
||||||
<div className="tableListHeader" onClick={this.onHeaderClick}>
|
{!this.props.headless && (
|
||||||
{this.getHeader()}
|
<div className="tableListHeader" onClick={this.onHeaderClick}>
|
||||||
</div>
|
{this.getHeader()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="tableListContent">{this.props.children}</div>
|
<div className="tableListContent">{this.props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -85,8 +85,12 @@ import trash from './icons/trash.svg';
|
|||||||
import unlink from './icons/unlink.svg';
|
import unlink from './icons/unlink.svg';
|
||||||
import user from './icons/user.svg';
|
import user from './icons/user.svg';
|
||||||
import youtube from './icons/youtube.svg';
|
import youtube from './icons/youtube.svg';
|
||||||
|
import eye from './icons/eye.svg';
|
||||||
|
import eyeClosed from './icons/eye-closed.svg';
|
||||||
|
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
|
eye,
|
||||||
|
'eye-closed': eyeClosed,
|
||||||
brush,
|
brush,
|
||||||
scissors,
|
scissors,
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye-slash" class="svg-inline--fa fa-eye-slash fa-w-20" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M320 400c-75.85 0-137.25-58.71-142.9-133.11L72.2 185.82c-13.79 17.3-26.48 35.59-36.72 55.59a32.35 32.35 0 0 0 0 29.19C89.71 376.41 197.07 448 320 448c26.91 0 52.87-4 77.89-10.46L346 397.39a144.13 144.13 0 0 1-26 2.61zm313.82 58.1l-110.55-85.44a331.25 331.25 0 0 0 81.25-102.07 32.35 32.35 0 0 0 0-29.19C550.29 135.59 442.93 64 320 64a308.15 308.15 0 0 0-147.32 37.7L45.46 3.37A16 16 0 0 0 23 6.18L3.37 31.45A16 16 0 0 0 6.18 53.9l588.36 454.73a16 16 0 0 0 22.46-2.81l19.64-25.27a16 16 0 0 0-2.82-22.45zm-183.72-142l-39.3-30.38A94.75 94.75 0 0 0 416 256a94.76 94.76 0 0 0-121.31-92.21A47.65 47.65 0 0 1 304 192a46.64 46.64 0 0 1-1.54 10l-73.61-56.89A142.31 142.31 0 0 1 320 112a143.92 143.92 0 0 1 144 144c0 21.63-5.29 41.79-13.9 60.11z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 974 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="eye" class="svg-inline--fa fa-eye fa-w-18" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M572.52 241.4C518.29 135.59 410.93 64 288 64S57.68 135.64 3.48 241.41a32.35 32.35 0 0 0 0 29.19C57.71 376.41 165.07 448 288 448s230.32-71.64 284.52-177.41a32.35 32.35 0 0 0 0-29.19zM288 400a144 144 0 1 1 144-144 143.93 143.93 0 0 1-144 144zm0-240a95.31 95.31 0 0 0-25.31 3.79 47.85 47.85 0 0 1-66.9 66.9A95.78 95.78 0 1 0 288 160z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 557 B |
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
|||||||
export function ViewportLoadingIndicator(props) {
|
export function ViewportLoadingIndicator(props) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="loadingIndicator"
|
className="loading-indicator"
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
|
|||||||
@@ -3,6 +3,43 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [3.8.19](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.18...@ohif/viewer@3.8.19) (2020-04-09)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file. See
|
||||||
|
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [3.8.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.17...@ohif/viewer@3.8.18) (2020-04-07)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.8.17](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.16...@ohif/viewer@3.8.17) (2020-04-06)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.8.16](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.15...@ohif/viewer@3.8.16) (2020-04-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [3.8.15](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.14...@ohif/viewer@3.8.15) (2020-04-02)
|
## [3.8.15](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.8.14...@ohif/viewer@3.8.15) (2020-04-02)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/viewer
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ describe('OHIF Save Measurements', function() {
|
|||||||
cy.get('@measurementsBtn').click();
|
cy.get('@measurementsBtn').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks if measurements of unsupported tools were not saved', function() {
|
/*it('checks if measurements of unsupported tools were not saved', function() {
|
||||||
// Add measurement for supported tool in the viewport
|
// Add measurement for supported tool in the viewport
|
||||||
cy.addLengthMeasurement();
|
cy.addLengthMeasurement();
|
||||||
// Add measurement for unsupported tool in the viewport
|
// Add measurement for unsupported tool in the viewport
|
||||||
@@ -146,5 +146,5 @@ describe('OHIF Save Measurements', function() {
|
|||||||
|
|
||||||
// Close Measurements panel
|
// Close Measurements panel
|
||||||
cy.get('@measurementsBtn').click();
|
cy.get('@measurementsBtn').click();
|
||||||
});
|
});*/
|
||||||
});
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/viewer",
|
"name": "@ohif/viewer",
|
||||||
"version": "3.8.15",
|
"version": "3.8.19",
|
||||||
"description": "OHIF Viewer",
|
"description": "OHIF Viewer",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -48,16 +48,17 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"@ohif/core": "^2.6.6",
|
"@ohif/core": "^2.6.9",
|
||||||
"@ohif/extension-cornerstone": "^2.5.1",
|
"@ohif/extension-cornerstone": "^2.5.2",
|
||||||
"@ohif/extension-dicom-html": "^1.1.2",
|
"@ohif/extension-dicom-html": "^1.1.2",
|
||||||
"@ohif/extension-dicom-microscopy": "^0.50.9",
|
"@ohif/extension-dicom-microscopy": "^0.50.9",
|
||||||
"@ohif/extension-dicom-pdf": "^1.0.3",
|
"@ohif/extension-dicom-pdf": "^1.0.3",
|
||||||
"@ohif/extension-dicom-segmentation": "^0.1.1",
|
"@ohif/extension-dicom-rt": "^0.0.2",
|
||||||
|
"@ohif/extension-dicom-segmentation": "^0.1.2",
|
||||||
"@ohif/extension-lesion-tracker": "^0.2.0",
|
"@ohif/extension-lesion-tracker": "^0.2.0",
|
||||||
"@ohif/extension-vtk": "^1.3.7",
|
"@ohif/extension-vtk": "^1.3.10",
|
||||||
"@ohif/i18n": "^0.52.7",
|
"@ohif/i18n": "^0.52.8",
|
||||||
"@ohif/ui": "^1.4.1",
|
"@ohif/ui": "^1.4.3",
|
||||||
"@tanem/react-nprogress": "^1.1.25",
|
"@tanem/react-nprogress": "^1.1.25",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.2.1",
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.8",
|
||||||
"cornerstone-tools": "4.12.5",
|
"cornerstone-tools": "4.12.5",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dcmjs": "^0.10.1",
|
"dcmjs": "^0.12.0",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
"dicomweb-client": "^0.4.4",
|
"dicomweb-client": "^0.4.4",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
|
|||||||
@@ -117,4 +117,12 @@ window.config = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
cornerstoneExtensionConfig: {},
|
cornerstoneExtensionConfig: {},
|
||||||
|
// Following property limits number of simultaneous series metadata requests.
|
||||||
|
// For http/1.x-only servers, set this to 5 or less to improve
|
||||||
|
// on first meaningful display in viewer
|
||||||
|
// If the server is particularly slow to respond to series metadata
|
||||||
|
// requests as it extracts the metadata from raw files everytime,
|
||||||
|
// try setting this to even lower value
|
||||||
|
// Leave it undefined for no limit, sutiable for HTTP/2 enabled servers
|
||||||
|
// maxConcurrentMetadataRequests: 5,
|
||||||
};
|
};
|
||||||
@@ -116,11 +116,16 @@ const _addSeriesToStudy = (studyMetadata, series) => {
|
|||||||
extensionManager.modules['sopClassHandlerModule'];
|
extensionManager.modules['sopClassHandlerModule'];
|
||||||
const study = studyMetadata.getData();
|
const study = studyMetadata.getData();
|
||||||
const seriesMetadata = new OHIFSeriesMetadata(series, study);
|
const seriesMetadata = new OHIFSeriesMetadata(series, study);
|
||||||
studyMetadata.addSeries(seriesMetadata);
|
const existingSeries = studyMetadata.getSeriesByUID(series.SeriesInstanceUID);
|
||||||
|
if (existingSeries) {
|
||||||
|
studyMetadata.updateSeries(series.SeriesInstanceUID, seriesMetadata);
|
||||||
|
} else {
|
||||||
|
studyMetadata.addSeries(seriesMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
studyMetadata.createAndAddDisplaySetsForSeries(
|
studyMetadata.createAndAddDisplaySetsForSeries(
|
||||||
sopClassHandlerModules,
|
sopClassHandlerModules,
|
||||||
seriesMetadata,
|
seriesMetadata,
|
||||||
false
|
|
||||||
);
|
);
|
||||||
study.displaySets = studyMetadata.getDisplaySets();
|
study.displaySets = studyMetadata.getDisplaySets();
|
||||||
_updateStudyMetadataManager(study, studyMetadata);
|
_updateStudyMetadataManager(study, studyMetadata);
|
||||||
@@ -148,25 +153,6 @@ const _updateStudyDisplaySets = (study, studyMetadata) => {
|
|||||||
const _sortStudyDisplaySet = (study, studyMetadata) => {
|
const _sortStudyDisplaySet = (study, studyMetadata) => {
|
||||||
studyMetadata.sortDisplaySets(study.displaySets);
|
studyMetadata.sortDisplaySets(study.displaySets);
|
||||||
};
|
};
|
||||||
const _loadRemainingSeries = studyMetadata => {
|
|
||||||
const { seriesLoader } = studyMetadata.getData();
|
|
||||||
if (!seriesLoader) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
const promisesLoaders = [];
|
|
||||||
while (seriesLoader.hasNext()) {
|
|
||||||
promisesLoaders.push(
|
|
||||||
seriesLoader
|
|
||||||
.next()
|
|
||||||
.then(
|
|
||||||
series => void _addSeriesToStudy(studyMetadata, series),
|
|
||||||
error => void log.error(error)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(promisesLoaders);
|
|
||||||
};
|
|
||||||
|
|
||||||
function ViewerRetrieveStudyData({
|
function ViewerRetrieveStudyData({
|
||||||
server,
|
server,
|
||||||
@@ -180,7 +166,10 @@ function ViewerRetrieveStudyData({
|
|||||||
const [isStudyLoaded, setIsStudyLoaded] = useState(false);
|
const [isStudyLoaded, setIsStudyLoaded] = useState(false);
|
||||||
const snackbarContext = useSnackbarContext();
|
const snackbarContext = useSnackbarContext();
|
||||||
const { appConfig = {} } = useContext(AppContext);
|
const { appConfig = {} } = useContext(AppContext);
|
||||||
const { filterQueryParam: isFilterStrategy = false } = appConfig;
|
const {
|
||||||
|
filterQueryParam: isFilterStrategy = false,
|
||||||
|
maxConcurrentMetadataRequests,
|
||||||
|
} = appConfig;
|
||||||
|
|
||||||
let cancelableSeriesPromises;
|
let cancelableSeriesPromises;
|
||||||
let cancelableStudiesPromises;
|
let cancelableStudiesPromises;
|
||||||
@@ -241,7 +230,7 @@ function ViewerRetrieveStudyData({
|
|||||||
|
|
||||||
// Attempt to load remaning series if any
|
// Attempt to load remaning series if any
|
||||||
cancelableSeriesPromises[study.StudyInstanceUID] = makeCancelable(
|
cancelableSeriesPromises[study.StudyInstanceUID] = makeCancelable(
|
||||||
_loadRemainingSeries(studyMetadata)
|
loadRemainingSeries(studyMetadata)
|
||||||
)
|
)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result && !result.isCanceled) {
|
if (result && !result.isCanceled) {
|
||||||
@@ -262,6 +251,27 @@ function ViewerRetrieveStudyData({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const forceRerender = () => setStudies(studies => [...studies]);
|
||||||
|
|
||||||
|
const loadRemainingSeries = async studyMetadata => {
|
||||||
|
const { seriesLoader } = studyMetadata.getData();
|
||||||
|
if (!seriesLoader) return;
|
||||||
|
|
||||||
|
const loadNextSeries = async () => {
|
||||||
|
if (!seriesLoader.hasNext()) return;
|
||||||
|
const series = await seriesLoader.next();
|
||||||
|
_addSeriesToStudy(studyMetadata, series);
|
||||||
|
forceRerender();
|
||||||
|
return loadNextSeries();
|
||||||
|
};
|
||||||
|
|
||||||
|
const concurrentRequestsAllowed = maxConcurrentMetadataRequests || studyMetadata.getSeriesCount();
|
||||||
|
const promises = Array(concurrentRequestsAllowed)
|
||||||
|
.fill(null)
|
||||||
|
.map(loadNextSeries);
|
||||||
|
await Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
const loadStudies = async () => {
|
const loadStudies = async () => {
|
||||||
try {
|
try {
|
||||||
const filters = {};
|
const filters = {};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import ReactDOM from 'react-dom';
|
|||||||
import OHIFVTKExtension from '@ohif/extension-vtk';
|
import OHIFVTKExtension from '@ohif/extension-vtk';
|
||||||
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
|
import OHIFDicomHtmlExtension from '@ohif/extension-dicom-html';
|
||||||
import OHIFDicomSegmentationExtension from '@ohif/extension-dicom-segmentation';
|
import OHIFDicomSegmentationExtension from '@ohif/extension-dicom-segmentation';
|
||||||
|
import OHIFDicomRtExtension from '@ohif/extension-dicom-rt';
|
||||||
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
import OHIFDicomMicroscopyExtension from '@ohif/extension-dicom-microscopy';
|
||||||
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ const appProps = {
|
|||||||
OHIFDicomMicroscopyExtension,
|
OHIFDicomMicroscopyExtension,
|
||||||
OHIFDicomPDFExtension,
|
OHIFDicomPDFExtension,
|
||||||
OHIFDicomSegmentationExtension,
|
OHIFDicomSegmentationExtension,
|
||||||
|
OHIFDicomRtExtension,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user