Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
383a0ef167 | ||
|
|
56bd49a0cd | ||
|
|
755b283574 | ||
|
|
a8a4fcee40 | ||
|
|
9892320ffb | ||
|
|
28befb1ea6 | ||
|
|
c554c5fac2 | ||
|
|
6e3a58931a | ||
|
|
5beb12a7fc | ||
|
|
1953118c8a | ||
|
|
7807f849f2 | ||
|
|
b426256df5 | ||
|
|
c9b7e97f9c |
No files matched your search
@@ -8,7 +8,9 @@ OHIF Viewer and its medical imaging data. This post covers one of many potential
|
|||||||
setups that accomplish that. Please note, noticably absent is user account
|
setups that accomplish that. Please note, noticably absent is user account
|
||||||
control.
|
control.
|
||||||
|
|
||||||
Do not use this recipe to host sensitive medical data on the open web. Depending
|
> **Do not use this recipe to host sensitive medical data on the open web!**
|
||||||
|
|
||||||
|
Depending
|
||||||
on your company's policies, this may be an appropriate setup on an internal
|
on your company's policies, this may be an appropriate setup on an internal
|
||||||
network when protected with a server's basic authentication. For a more robust
|
network when protected with a server's basic authentication. For a more robust
|
||||||
setup, check out our [user account control recipe](./user-account-control.md)
|
setup, check out our [user account control recipe](./user-account-control.md)
|
||||||
@@ -23,13 +25,13 @@ Our two biggest hurdles when hosting our image archive and web client are:
|
|||||||
|
|
||||||
### Handling Web Requests
|
### Handling Web Requests
|
||||||
|
|
||||||
We mittigate our first issue by allowing [Nginx][nginx] to handle incoming web
|
We mitigate our first issue by allowing [Nginx][nginx] to handle incoming web
|
||||||
requests. Nginx is open source software for web serving, reverse proxying,
|
requests. Nginx is open source software for web serving, reverse proxying,
|
||||||
caching, and more. It's designed for maximum performance and stability --
|
caching, and more. It's designed for maximum performance and stability --
|
||||||
allowing us to more reliably serve content than Orthanc's built-in server can.
|
allowing us to more reliably serve content than Orthanc's built-in server can.
|
||||||
|
|
||||||
More specifically, we accomplish this by using a
|
More specifically, we accomplish this by using a
|
||||||
[`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) to retrieve
|
[`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) to retrieve
|
||||||
resources from our image archive (Orthanc), and when accessing its web admin.
|
resources from our image archive (Orthanc), and when accessing its web admin.
|
||||||
|
|
||||||
> A reverse proxy is a type of proxy server that retrieves resources on behalf
|
> A reverse proxy is a type of proxy server that retrieves resources on behalf
|
||||||
@@ -38,7 +40,7 @@ resources from our image archive (Orthanc), and when accessing its web admin.
|
|||||||
|
|
||||||
### CORS Issues
|
### CORS Issues
|
||||||
|
|
||||||
Cross-Origin Resource Sharing (CORS) is a mechanism that uses HTTP headers to
|
[Cross-Origin Resource Sharing][understanding-cors] (CORS) is a mechanism that uses HTTP headers to
|
||||||
tell a browser which web applications have permission to access selected
|
tell a browser which web applications have permission to access selected
|
||||||
resources from a server at a different origin (domain, protocol, port). IE. By
|
resources from a server at a different origin (domain, protocol, port). IE. By
|
||||||
default, a Web App located at `http://my-website.com` can't access resources
|
default, a Web App located at `http://my-website.com` can't access resources
|
||||||
@@ -51,9 +53,6 @@ We can solve this one of two ways:
|
|||||||
|
|
||||||
**This solution uses the first approach.**
|
**This solution uses the first approach.**
|
||||||
|
|
||||||
You can read more about CORS in this Medium article: [Understanding
|
|
||||||
CORS][understanding-cors]
|
|
||||||
|
|
||||||
### Diagram
|
### Diagram
|
||||||
|
|
||||||
This setup allows us to create a setup similar to the one pictured below:
|
This setup allows us to create a setup similar to the one pictured below:
|
||||||
@@ -73,6 +72,8 @@ This setup allows us to create a setup similar to the one pictured below:
|
|||||||
- Docker
|
- Docker
|
||||||
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
|
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
|
||||||
- [Docker for Windows](https://docs.docker.com/docker-for-windows/)
|
- [Docker for Windows](https://docs.docker.com/docker-for-windows/)
|
||||||
|
- Node 16.x with optional Python and Visual Studio Build Tools ([webpack 4 has an issue](https://github.com/webpack/webpack/issues/14532) with Node 17.x)
|
||||||
|
- [Node.js](https://nodejs.org/en/download/)
|
||||||
|
|
||||||
_Not sure if you have `docker` installed already? Try running `docker --version`
|
_Not sure if you have `docker` installed already? Try running `docker --version`
|
||||||
in command prompt or terminal_
|
in command prompt or terminal_
|
||||||
@@ -81,7 +82,7 @@ in command prompt or terminal_
|
|||||||
|
|
||||||
_Spin Things Up_
|
_Spin Things Up_
|
||||||
|
|
||||||
- Navigate to `<project-root>/docker/OpenResty-Orthanc` in your shell
|
- Navigate to `./platform/viewer/.recipes/OpenResty-Orthanc` in your shell
|
||||||
- Run `docker-compose up`
|
- Run `docker-compose up`
|
||||||
|
|
||||||
_Upload Your First Study_
|
_Upload Your First Study_
|
||||||
@@ -94,16 +95,16 @@ _Upload Your First Study_
|
|||||||
|
|
||||||
### Troubleshooting
|
### Troubleshooting
|
||||||
|
|
||||||
_Exit code 137_
|
#### Exit code 137
|
||||||
|
|
||||||
This means Docker ran out of memory. Open Docker Desktop, go to the `advanced`
|
This means Docker ran out of memory. Open Docker Desktop, go to the `advanced`
|
||||||
tab, and increase the amount of Memory available.
|
tab, and increase the amount of Memory available.
|
||||||
|
|
||||||
_Cannot create container for service X_
|
#### Cannot create container for service X
|
||||||
|
|
||||||
Use this one with caution: `docker system prune`
|
Use this one with caution: `docker system prune`
|
||||||
|
|
||||||
_X is already running_
|
#### X is already running
|
||||||
|
|
||||||
Stop running all containers:
|
Stop running all containers:
|
||||||
|
|
||||||
@@ -120,15 +121,12 @@ likely want to update:
|
|||||||
#### OHIF Viewer
|
#### OHIF Viewer
|
||||||
|
|
||||||
The OHIF Viewer's configuration is imported from a static `.js` file. The
|
The OHIF Viewer's configuration is imported from a static `.js` file. The
|
||||||
configuration we use is set to a specific file when we build the viewer, and
|
configuration we use is set to [a specific file][config-ohif] when we build the viewer, and
|
||||||
determined by the env variable: `APP_CONFIG`. You can see where we set its value
|
determined by the env variable: `APP_CONFIG`. You can see where we set its value
|
||||||
in the `dockerfile` for this solution:
|
in the `dockerfile` for this solution:
|
||||||
|
|
||||||
`ENV APP_CONFIG=config/docker_openresty-orthanc.js`
|
`ENV APP_CONFIG=config/docker_openresty-orthanc.js`
|
||||||
|
|
||||||
You can find the configuration we're using here:
|
|
||||||
`/public/config/docker_openresty-orthanc.js`
|
|
||||||
|
|
||||||
To rebuild the `webapp` image created by our `dockerfile` after updating the
|
To rebuild the `webapp` image created by our `dockerfile` after updating the
|
||||||
Viewer's configuration, you can run:
|
Viewer's configuration, you can run:
|
||||||
|
|
||||||
@@ -137,7 +135,7 @@ Viewer's configuration, you can run:
|
|||||||
|
|
||||||
#### Other
|
#### Other
|
||||||
|
|
||||||
All other files are found in: `/docker/OpenResty-Orthanc/`
|
All other files in this directory include
|
||||||
|
|
||||||
| Service | Configuration | Docs |
|
| Service | Configuration | Docs |
|
||||||
| ----------------- | --------------------------------- | ------------------------------------------- |
|
| ----------------- | --------------------------------- | ------------------------------------------- |
|
||||||
@@ -236,6 +234,8 @@ members put together:
|
|||||||
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
|
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
|
||||||
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
|
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
|
||||||
<!-- SRC -->
|
<!-- SRC -->
|
||||||
|
|
||||||
|
[config-ohif]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/docker_openresty-orthanc.js
|
||||||
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
|
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
|
||||||
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/nginx.conf
|
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/nginx.conf
|
||||||
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/orthanc.json
|
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/orthanc.json
|
||||||
|
|||||||
@@ -3,6 +3,30 @@
|
|||||||
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.12.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.6...@ohif/extension-cornerstone@2.12.7) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.12.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.5...@ohif/extension-cornerstone@2.12.6) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.12.5](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.4...@ohif/extension-cornerstone@2.12.5) (2022-02-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.12.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.3...@ohif/extension-cornerstone@2.12.4) (2022-01-20)
|
## [2.12.4](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.12.3...@ohif/extension-cornerstone@2.12.4) (2022-01-20)
|
||||||
|
|
||||||
**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.12.4",
|
"version": "2.12.7",
|
||||||
"description": "OHIF extension for Cornerstone",
|
"description": "OHIF extension for Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"cornerstone-math": "^0.1.9",
|
"cornerstone-math": "^0.1.9",
|
||||||
"cornerstone-tools": "^6.0.6",
|
"cornerstone-tools": "^6.0.6",
|
||||||
"cornerstone-wado-image-loader": "^4.1.0",
|
"cornerstone-wado-image-loader": "^4.1.0",
|
||||||
"dcmjs": "0.19.2",
|
"dcmjs": "0.19.4",
|
||||||
"dicom-parser": "^1.8.11",
|
"dicom-parser": "^1.8.11",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import OHIF from '@ohif/core';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import throttle from 'lodash.throttle';
|
import throttle from 'lodash.throttle';
|
||||||
import { setEnabledElement } from './state';
|
import { setEnabledElement } from './state';
|
||||||
|
import initSRTools from './tools/initSRTools';
|
||||||
|
|
||||||
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
||||||
const {
|
const {
|
||||||
@@ -92,6 +93,7 @@ const mapDispatchToProps = (dispatch, ownProps) => {
|
|||||||
plugin: 'cornerstone',
|
plugin: 'cornerstone',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
initSRTools(enabledElement);
|
||||||
},
|
},
|
||||||
|
|
||||||
onMeasurementsChanged: (event, action) => {
|
onMeasurementsChanged: (event, action) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ConnectedCornerstoneViewport from './ConnectedCornerstoneViewport';
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import checkForSRAnnotations from './tools/checkForSRAnnotations';
|
||||||
|
|
||||||
const { StackManager } = OHIF.utils;
|
const { StackManager } = OHIF.utils;
|
||||||
|
|
||||||
@@ -187,6 +188,8 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
||||||
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
displaySet.frameIndex !== prevDisplaySet.frameIndex
|
||||||
) {
|
) {
|
||||||
|
const { viewportIndex } = this.props;
|
||||||
|
checkForSRAnnotations({ displaySet, viewportIndex });
|
||||||
this.setStateFromProps();
|
this.setStateFromProps();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import csTools from 'cornerstone-tools';
|
|||||||
import merge from 'lodash.merge';
|
import merge from 'lodash.merge';
|
||||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||||
|
import dicomSRModule from './tools/modules/dicomSRModule';
|
||||||
|
import srModuleId from './tools/id';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@@ -15,6 +17,8 @@ import measurementServiceMappingsFactory from './utils/measurementServiceMapping
|
|||||||
export default function init({ servicesManager, configuration }) {
|
export default function init({ servicesManager, configuration }) {
|
||||||
const { UIDialogService, MeasurementService } = servicesManager.services;
|
const { UIDialogService, MeasurementService } = servicesManager.services;
|
||||||
|
|
||||||
|
csTools.register('module', srModuleId, dicomSRModule);
|
||||||
|
|
||||||
const callInputDialog = (data, event, callback) => {
|
const callInputDialog = (data, event, callback) => {
|
||||||
if (UIDialogService) {
|
if (UIDialogService) {
|
||||||
let dialogId = UIDialogService.create({
|
let dialogId = UIDialogService.create({
|
||||||
|
|||||||
@@ -0,0 +1,414 @@
|
|||||||
|
import csTools, {
|
||||||
|
importInternal,
|
||||||
|
getToolState,
|
||||||
|
toolColors,
|
||||||
|
} from 'cornerstone-tools';
|
||||||
|
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
|
/** Internal imports */
|
||||||
|
import TOOL_NAMES from './constants/toolNames';
|
||||||
|
import SCOORD_TYPES from './constants/scoordTypes';
|
||||||
|
import id from './id';
|
||||||
|
|
||||||
|
/** Cornerstone 3rd party dev kit imports */
|
||||||
|
const draw = importInternal('drawing/draw');
|
||||||
|
const drawJoinedLines = importInternal('drawing/drawJoinedLines');
|
||||||
|
const drawCircle = importInternal('drawing/drawCircle');
|
||||||
|
const drawEllipse = importInternal('drawing/drawEllipse');
|
||||||
|
const drawHandles = importInternal('drawing/drawHandles');
|
||||||
|
const drawArrow = importInternal('drawing/drawArrow');
|
||||||
|
const getNewContext = importInternal('drawing/getNewContext');
|
||||||
|
const BaseTool = importInternal('base/BaseTool');
|
||||||
|
const drawLinkedTextBox = importInternal('drawing/drawLinkedTextBox');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class DICOMSRDisplayTool - Renders DICOMSR data in a read only manner (i.e. as an overlay).
|
||||||
|
*
|
||||||
|
* This is a generic render tool.
|
||||||
|
*
|
||||||
|
* A single tool that, given some schema, can render
|
||||||
|
* POINT, MULTIPOINT, POLYLINE, CIRCLE, and ELLIPSE
|
||||||
|
* value types for a given imageId.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @extends cornerstoneTools.BaseTool
|
||||||
|
*/
|
||||||
|
export default class DICOMSRDisplayTool extends BaseTool {
|
||||||
|
constructor(props = {}) {
|
||||||
|
const defaultProps = {
|
||||||
|
mixins: ['enabledOrDisabledBinaryTool'],
|
||||||
|
name: TOOL_NAMES.DICOM_SR_DISPLAY_TOOL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialProps = Object.assign(defaultProps, props);
|
||||||
|
|
||||||
|
super(initialProps);
|
||||||
|
|
||||||
|
this._module = csTools.getModule(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderToolData(evt) {
|
||||||
|
const eventData = evt.detail;
|
||||||
|
const { element } = eventData;
|
||||||
|
const module = this._module;
|
||||||
|
|
||||||
|
const toolState = getToolState(element, this.name);
|
||||||
|
|
||||||
|
if (!toolState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackingUniqueIdentifiersForElement = module.getters.trackingUniqueIdentifiersForElement(
|
||||||
|
element
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
activeIndex,
|
||||||
|
trackingUniqueIdentifiers,
|
||||||
|
} = trackingUniqueIdentifiersForElement;
|
||||||
|
|
||||||
|
const activeTrackingUniqueIdentifier =
|
||||||
|
trackingUniqueIdentifiers[activeIndex];
|
||||||
|
|
||||||
|
// Filter toolData to only render the data for the active SR.
|
||||||
|
const filteredToolData = toolState.data.filter(td =>
|
||||||
|
trackingUniqueIdentifiers.includes(td.TrackingUniqueIdentifier)
|
||||||
|
);
|
||||||
|
|
||||||
|
let shouldRepositionTextBoxes = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < filteredToolData.length; i++) {
|
||||||
|
const data = filteredToolData[i];
|
||||||
|
const { renderableData, labels } = data;
|
||||||
|
|
||||||
|
const color =
|
||||||
|
data.TrackingUniqueIdentifier === activeTrackingUniqueIdentifier
|
||||||
|
? toolColors.getActiveColor()
|
||||||
|
: toolColors.getToolColor();
|
||||||
|
const lineWidth = 2;
|
||||||
|
const options = {
|
||||||
|
color,
|
||||||
|
lineWidth,
|
||||||
|
handleRadius: 6,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(renderableData).forEach(GraphicType => {
|
||||||
|
const renderableDataForGraphicType = renderableData[GraphicType];
|
||||||
|
|
||||||
|
switch (GraphicType) {
|
||||||
|
case SCOORD_TYPES.POINT:
|
||||||
|
this.renderPoint(renderableDataForGraphicType, eventData, options);
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.MULTIPOINT:
|
||||||
|
this.renderMultipoint(
|
||||||
|
renderableDataForGraphicType,
|
||||||
|
eventData,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.POLYGON:
|
||||||
|
case SCOORD_TYPES.POLYLINE:
|
||||||
|
this.renderPolyLine(
|
||||||
|
renderableDataForGraphicType,
|
||||||
|
eventData,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.CIRCLE:
|
||||||
|
this.renderCircle(renderableDataForGraphicType, eventData, options);
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.ELLIPSE:
|
||||||
|
this.renderEllipse(
|
||||||
|
renderableDataForGraphicType,
|
||||||
|
eventData,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { element } = eventData;
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
if (!data.handles || !data.handles.textBox) {
|
||||||
|
const textBox = {
|
||||||
|
active: false,
|
||||||
|
hasMoved: true,
|
||||||
|
movesIndependently: false,
|
||||||
|
drawnIndependently: true,
|
||||||
|
allowedOutsideImage: true,
|
||||||
|
hasBoundingBox: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const anchorPoints = _getTextBoxAnchorPointsForRenderableData(
|
||||||
|
renderableData,
|
||||||
|
eventData
|
||||||
|
);
|
||||||
|
textBox.anchorPoints = anchorPoints;
|
||||||
|
|
||||||
|
const bottomRight = {
|
||||||
|
x: Math.max(...anchorPoints.map(point => point.x)),
|
||||||
|
y: Math.max(...anchorPoints.map(point => point.y)),
|
||||||
|
};
|
||||||
|
|
||||||
|
textBox.x = bottomRight.x;
|
||||||
|
textBox.y = bottomRight.y;
|
||||||
|
|
||||||
|
data.handles = {};
|
||||||
|
data.handles.textBox = textBox;
|
||||||
|
|
||||||
|
shouldRepositionTextBoxes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = _getTextBoxLinesFromLabels(labels);
|
||||||
|
|
||||||
|
function textBoxAnchorPoints() {
|
||||||
|
return data.handles.textBox.anchorPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(context, context => {
|
||||||
|
drawLinkedTextBox(
|
||||||
|
context,
|
||||||
|
element,
|
||||||
|
data.handles.textBox,
|
||||||
|
text,
|
||||||
|
data.handles,
|
||||||
|
textBoxAnchorPoints,
|
||||||
|
color,
|
||||||
|
lineWidth,
|
||||||
|
0,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOOD -> text boxes may overlap with other annotations at the moment.
|
||||||
|
// To be fixed after we get requirements.
|
||||||
|
// if (shouldRepositionTextBoxes) {
|
||||||
|
// this.repositionTextBox(filteredToolData, eventData);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
// repositionTextBox(toolData, eventData) {
|
||||||
|
// const toolBoundingBoxes = [];
|
||||||
|
|
||||||
|
// for (let i = 0; i < toolData.length; i++) {
|
||||||
|
// const toolDataI = toolData[i];
|
||||||
|
|
||||||
|
// const { textBox } = toolDataI.handles;
|
||||||
|
// const { anchorPoints } = textBox;
|
||||||
|
|
||||||
|
// const boundingBox = _getBoundingBoxFromAnchorPoints(anchorPoints);
|
||||||
|
// // Get the textbox bounding locations.
|
||||||
|
// // Get the tool extents.
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
renderPolyLine(renderableData, eventData, options) {
|
||||||
|
const { element } = eventData;
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
renderableData.forEach(points => {
|
||||||
|
draw(context, context => {
|
||||||
|
drawJoinedLines(context, element, points[0], points, options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMultipoint(renderableData, eventData, options) {
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
renderableData.forEach(points => {
|
||||||
|
draw(context, context => {
|
||||||
|
drawHandles(context, eventData, points, options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPoint(renderableData, eventData, options) {
|
||||||
|
// Render single point as an arrow.
|
||||||
|
const { element, image } = eventData;
|
||||||
|
const { rows, columns } = image;
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
const { color, lineWidth } = options;
|
||||||
|
|
||||||
|
// Find a suitable length for the image size.
|
||||||
|
|
||||||
|
const xOffset = columns / 10;
|
||||||
|
const yOffset = rows / 10;
|
||||||
|
|
||||||
|
renderableData.forEach(points => {
|
||||||
|
const point = points[0]; // The SCOORD type is POINT so the array length is 1.
|
||||||
|
draw(context, context => {
|
||||||
|
// Draw the arrow
|
||||||
|
const handleStartCanvas = cornerstone.pixelToCanvas(element, point);
|
||||||
|
const handleEndCanvas = cornerstone.pixelToCanvas(element, {
|
||||||
|
x: point.x + xOffset,
|
||||||
|
y: point.y + yOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
drawArrow(
|
||||||
|
context,
|
||||||
|
handleEndCanvas,
|
||||||
|
handleStartCanvas,
|
||||||
|
color,
|
||||||
|
lineWidth,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCircle(renderableData, eventData, options) {
|
||||||
|
const { element } = eventData;
|
||||||
|
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
renderableData.forEach(circle => {
|
||||||
|
const { center, radius } = circle;
|
||||||
|
|
||||||
|
drawCircle(context, element, center, radius, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEllipse(renderableData, eventData, options) {
|
||||||
|
const { element } = eventData;
|
||||||
|
|
||||||
|
const context = getNewContext(eventData.canvasContext.canvas);
|
||||||
|
|
||||||
|
renderableData.forEach(ellipse => {
|
||||||
|
const { corner1, corner2 } = ellipse;
|
||||||
|
|
||||||
|
drawEllipse(
|
||||||
|
context,
|
||||||
|
element,
|
||||||
|
corner1,
|
||||||
|
corner2,
|
||||||
|
options,
|
||||||
|
'pixel',
|
||||||
|
0 // TODO -> Work our the initial rotation and add it here so we render appropriately rotated ellipses.
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getTextBoxLinesFromLabels(labels) {
|
||||||
|
// TODO -> max 3 for now (label + shortAxis + longAxis), need a generic solution for this!
|
||||||
|
|
||||||
|
const labelLength = Math.min(labels.length, 3);
|
||||||
|
const lines = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < labelLength; i++) {
|
||||||
|
const labelEntry = labels[i];
|
||||||
|
lines.push(`${_labelToShorthand(labelEntry.label)}${labelEntry.value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHORT_HAND_MAP = {
|
||||||
|
'Short Axis': 'W ',
|
||||||
|
'Long Axis': 'L ',
|
||||||
|
AREA: 'Area ',
|
||||||
|
Length: '',
|
||||||
|
CORNERSTONEFREETEXT: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
function _labelToShorthand(label) {
|
||||||
|
const shortHand = SHORT_HAND_MAP[label];
|
||||||
|
|
||||||
|
if (shortHand !== undefined) {
|
||||||
|
return shortHand;
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getTextBoxAnchorPointsForRenderableData(renderableData, eventData) {
|
||||||
|
let anchorPoints = [];
|
||||||
|
|
||||||
|
Object.keys(renderableData).forEach(GraphicType => {
|
||||||
|
const renderableDataForGraphicType = renderableData[GraphicType];
|
||||||
|
|
||||||
|
switch (GraphicType) {
|
||||||
|
case SCOORD_TYPES.POINT:
|
||||||
|
renderableDataForGraphicType.forEach(points => {
|
||||||
|
anchorPoints = [...anchorPoints, ...points];
|
||||||
|
|
||||||
|
// Add other arrow point based on image size.
|
||||||
|
const { image } = eventData;
|
||||||
|
const { rows, columns } = image;
|
||||||
|
|
||||||
|
const xOffset = columns / 10;
|
||||||
|
const yOffset = rows / 10;
|
||||||
|
const point = points[0];
|
||||||
|
|
||||||
|
anchorPoints.push({ x: point.x + xOffset, y: point.y + yOffset });
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.MULTIPOINT:
|
||||||
|
case SCOORD_TYPES.POLYLINE:
|
||||||
|
case SCOORD_TYPES.POLYGON:
|
||||||
|
renderableDataForGraphicType.forEach(points => {
|
||||||
|
anchorPoints = [...anchorPoints, ...points];
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.CIRCLE:
|
||||||
|
renderableDataForGraphicType.forEach(circle => {
|
||||||
|
const { center, radius } = circle;
|
||||||
|
|
||||||
|
anchorPoints.push({ x: center.x + radius, y: center.y });
|
||||||
|
anchorPoints.push({ x: center.x - radius, y: center.y });
|
||||||
|
anchorPoints.push({ x: center.x, y: center.y + radius });
|
||||||
|
anchorPoints.push({ x: center.x, y: center.y - radius });
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.ELLIPSE:
|
||||||
|
renderableDataForGraphicType.forEach(ellipse => {
|
||||||
|
const { corner1, corner2 } = ellipse;
|
||||||
|
|
||||||
|
const halfWidth = Math.abs(corner1.x - corner2.x) / 2;
|
||||||
|
const halfHeight = Math.abs(corner1.y - corner2.y) / 2;
|
||||||
|
|
||||||
|
const center = {
|
||||||
|
x: (corner1.x + corner2.x) / 2,
|
||||||
|
y: (corner1.y + corner2.y) / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
anchorPoints.push({ x: center.x + halfWidth, y: center.y });
|
||||||
|
anchorPoints.push({ x: center.x - halfWidth, y: center.y });
|
||||||
|
anchorPoints.push({ x: center.x, y: center.y + halfHeight });
|
||||||
|
anchorPoints.push({ x: center.x, y: center.y - halfHeight });
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return anchorPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getBoundingBoxFromAnchorPoints(anchorPoints) {
|
||||||
|
let minX = Infinity;
|
||||||
|
let maxX = -Infinity;
|
||||||
|
let minY = Infinity;
|
||||||
|
let maxY = -Infinity;
|
||||||
|
|
||||||
|
anchorPoints.forEach(point => {
|
||||||
|
const { x, y } = point;
|
||||||
|
|
||||||
|
if (x > maxX) {
|
||||||
|
maxX = x;
|
||||||
|
} else if (x < minX) {
|
||||||
|
minX = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y > maxX) {
|
||||||
|
maxY = y;
|
||||||
|
} else if (y < minY) {
|
||||||
|
minY = y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import csTools from 'cornerstone-tools';
|
||||||
|
import cs from 'cornerstone-core';
|
||||||
|
import OHIF from '@ohif/core';
|
||||||
|
|
||||||
|
import { getEnabledElement } from '../state';
|
||||||
|
import id from './id';
|
||||||
|
|
||||||
|
const { studyMetadataManager } = OHIF.utils;
|
||||||
|
|
||||||
|
const checkForSRAnnotations = ({ viewportIndex, displaySet }) => {
|
||||||
|
const srModule = csTools.getModule(id);
|
||||||
|
|
||||||
|
const element = getEnabledElement(viewportIndex);
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { StudyInstanceUID } = displaySet;
|
||||||
|
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||||
|
if (!studyMetadata) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const srDisplaySets = studyMetadata
|
||||||
|
.getDisplaySets()
|
||||||
|
.filter(ds => ds.Modality === 'SR');
|
||||||
|
if (srDisplaySets.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { measurements: _measurements } = srDisplaySets[0];
|
||||||
|
if (!_measurements || _measurements.length < 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const measurements = _measurements.filter(m => m.loaded === true);
|
||||||
|
const measurement = measurements[0];
|
||||||
|
if (!measurement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
srModule.setters.trackingUniqueIdentifiersForElement(
|
||||||
|
element,
|
||||||
|
measurements.map(measurement => measurement.TrackingUniqueIdentifier),
|
||||||
|
measurement
|
||||||
|
);
|
||||||
|
|
||||||
|
const { TrackingUniqueIdentifier } = measurement;
|
||||||
|
srModule.setters.activeTrackingUniqueIdentifierForElement(
|
||||||
|
element,
|
||||||
|
TrackingUniqueIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
|
cs.updateImage(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default checkForSRAnnotations;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
POINT: 'POINT',
|
||||||
|
MULTIPOINT: 'MULTIPOINT',
|
||||||
|
POLYLINE: 'POLYLINE',
|
||||||
|
CIRCLE: 'CIRCLE',
|
||||||
|
ELLIPSE: 'ELLIPSE',
|
||||||
|
POLYGON: 'POLYGON',
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const TOOL_NAMES = {
|
||||||
|
DICOM_SR_DISPLAY_TOOL: 'DICOMSRDisplayTool',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TOOL_NAMES;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const id = 'org.ohif.dicom-sr';
|
||||||
|
|
||||||
|
export default id;
|
||||||
|
|
||||||
|
const SOPClassHandlerName = 'dicom-sr';
|
||||||
|
const SOPClassHandlerId = `${id}.sopClassHandlerModule.${SOPClassHandlerName}`;
|
||||||
|
export { SOPClassHandlerName, SOPClassHandlerId };
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
import DICOMSRDisplayTool from './DICOMSRDisplayTool';
|
||||||
|
import TOOL_NAMES from './constants/toolNames';
|
||||||
|
import getToolAlias from './utils/getToolAlias';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize SR cornerstone tools.
|
||||||
|
*
|
||||||
|
* @param {*} targetElement
|
||||||
|
*/
|
||||||
|
const initSRTools = targetElement => {
|
||||||
|
const primaryToolId = 'Wwwc';
|
||||||
|
const toolAlias = getToolAlias(primaryToolId); // These are 1:1 for built-in only
|
||||||
|
|
||||||
|
// ~~ MAGIC
|
||||||
|
cornerstoneTools.addToolForElement(targetElement, DICOMSRDisplayTool);
|
||||||
|
cornerstoneTools.setToolEnabledForElement(
|
||||||
|
targetElement,
|
||||||
|
TOOL_NAMES.DICOM_SR_DISPLAY_TOOL
|
||||||
|
);
|
||||||
|
|
||||||
|
// ~~ Variants
|
||||||
|
cornerstoneTools.addToolForElement(
|
||||||
|
targetElement,
|
||||||
|
cornerstoneTools.LengthTool,
|
||||||
|
{
|
||||||
|
name: 'SRLength',
|
||||||
|
configuration: {
|
||||||
|
renderDashed: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
cornerstoneTools.addToolForElement(
|
||||||
|
targetElement,
|
||||||
|
cornerstoneTools.ArrowAnnotateTool,
|
||||||
|
{
|
||||||
|
name: 'SRArrowAnnotate',
|
||||||
|
configuration: {
|
||||||
|
renderDashed: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
cornerstoneTools.addToolForElement(
|
||||||
|
targetElement,
|
||||||
|
cornerstoneTools.BidirectionalTool,
|
||||||
|
{
|
||||||
|
name: 'SRBidirectional',
|
||||||
|
configuration: {
|
||||||
|
renderDashed: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
cornerstoneTools.addToolForElement(
|
||||||
|
targetElement,
|
||||||
|
cornerstoneTools.EllipticalRoiTool,
|
||||||
|
{
|
||||||
|
name: 'SREllipticalRoi',
|
||||||
|
configuration: {
|
||||||
|
renderDashed: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
cornerstoneTools.addToolForElement(
|
||||||
|
targetElement,
|
||||||
|
cornerstoneTools.RectangleRoiTool,
|
||||||
|
{
|
||||||
|
name: 'SRRectangleRoi',
|
||||||
|
configuration: {
|
||||||
|
renderDashed: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
cornerstoneTools.addToolForElement(
|
||||||
|
targetElement,
|
||||||
|
cornerstoneTools.FreehandRoiTool,
|
||||||
|
{
|
||||||
|
name: 'SRFreehandRoi',
|
||||||
|
configuration: {
|
||||||
|
renderDashed: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ~~ Business as usual
|
||||||
|
cornerstoneTools.setToolActiveForElement(targetElement, 'PanMultiTouch', {
|
||||||
|
pointers: 2,
|
||||||
|
});
|
||||||
|
cornerstoneTools.setToolActiveForElement(targetElement, 'ZoomTouchPinch', {});
|
||||||
|
|
||||||
|
// TODO: Add always dashed tool alternative aliases
|
||||||
|
// TODO: or same name... alternative config?
|
||||||
|
cornerstoneTools.setToolActiveForElement(targetElement, toolAlias, {
|
||||||
|
mouseButtonMask: 1,
|
||||||
|
});
|
||||||
|
cornerstoneTools.setToolActiveForElement(targetElement, 'Pan', {
|
||||||
|
mouseButtonMask: 4,
|
||||||
|
});
|
||||||
|
cornerstoneTools.setToolActiveForElement(targetElement, 'Zoom', {
|
||||||
|
mouseButtonMask: 2,
|
||||||
|
});
|
||||||
|
cornerstoneTools.setToolActiveForElement(
|
||||||
|
targetElement,
|
||||||
|
'StackScrollMouseWheel',
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default initSRTools;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
TrackingUniqueIdentifier: null,
|
||||||
|
trackingIdentifiersByEnabledElementUUID: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
function setTrackingUniqueIdentifiersForElement(
|
||||||
|
element,
|
||||||
|
trackingUniqueIdentifiers,
|
||||||
|
activeIndex = 0
|
||||||
|
) {
|
||||||
|
const enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
const { uuid } = enabledElement;
|
||||||
|
|
||||||
|
state.trackingIdentifiersByEnabledElementUUID[uuid] = {
|
||||||
|
trackingUniqueIdentifiers,
|
||||||
|
activeIndex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveTrackingUniqueIdentifierForElement(
|
||||||
|
element,
|
||||||
|
TrackingUniqueIdentifier
|
||||||
|
) {
|
||||||
|
const enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
const { uuid } = enabledElement;
|
||||||
|
|
||||||
|
const trackingIdentifiersForElement =
|
||||||
|
state.trackingIdentifiersByEnabledElementUUID[uuid];
|
||||||
|
|
||||||
|
if (trackingIdentifiersForElement) {
|
||||||
|
const activeIndex = trackingIdentifiersForElement.trackingUniqueIdentifiers.findIndex(
|
||||||
|
tuid => tuid === TrackingUniqueIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
|
trackingIdentifiersForElement.activeIndex = activeIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTrackingUniqueIdentifiersForElement(element) {
|
||||||
|
const enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
const { uuid } = enabledElement;
|
||||||
|
|
||||||
|
if (state.trackingIdentifiersByEnabledElementUUID[uuid]) {
|
||||||
|
return state.trackingIdentifiersByEnabledElementUUID[uuid];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { trackingUniqueIdentifiers: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
state,
|
||||||
|
getters: {
|
||||||
|
trackingUniqueIdentifiersForElement: getTrackingUniqueIdentifiersForElement,
|
||||||
|
},
|
||||||
|
setters: {
|
||||||
|
trackingUniqueIdentifiersForElement: setTrackingUniqueIdentifiersForElement,
|
||||||
|
activeTrackingUniqueIdentifierForElement: setActiveTrackingUniqueIdentifierForElement,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Get cornerstone tool alias.
|
||||||
|
*
|
||||||
|
* @param {string} toolName
|
||||||
|
* @returns tool alias
|
||||||
|
*/
|
||||||
|
export default function getToolAlias(toolName) {
|
||||||
|
let toolAlias = toolName;
|
||||||
|
|
||||||
|
switch (toolName) {
|
||||||
|
case 'Length':
|
||||||
|
toolAlias = 'SRLength';
|
||||||
|
break;
|
||||||
|
case 'Bidirectional':
|
||||||
|
toolAlias = 'SRBidirectional';
|
||||||
|
break;
|
||||||
|
case 'ArrowAnnotate':
|
||||||
|
toolAlias = 'SRArrowAnnotate';
|
||||||
|
break;
|
||||||
|
case 'EllipticalRoi':
|
||||||
|
toolAlias = 'SREllipticalRoi';
|
||||||
|
break;
|
||||||
|
case 'FreehandRoi':
|
||||||
|
toolAlias = 'SRFreehandRoi';
|
||||||
|
break;
|
||||||
|
case 'RectangleRoi':
|
||||||
|
toolAlias = 'SRRectangleRoi';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return toolAlias;
|
||||||
|
}
|
||||||
@@ -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.3.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.3.1...@ohif/extension-debugging@0.3.2) (2022-03-10)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-debugging
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.3.0...@ohif/extension-debugging@0.3.1) (2021-12-02)
|
## [0.3.1](https://github.com/OHIF/Viewers/compare/@ohif/extension-debugging@0.3.0...@ohif/extension-debugging@0.3.1) (2021-12-02)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-debugging
|
**Note:** Version bump only for package @ohif/extension-debugging
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-debugging",
|
"name": "@ohif/extension-debugging",
|
||||||
"version": "0.3.1",
|
"version": "0.3.2",
|
||||||
"description": "OHIF extension for debugging.",
|
"description": "OHIF extension for debugging.",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -24,3 +24,21 @@
|
|||||||
overflow: scroll;
|
overflow: scroll;
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.debug-overflowText {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 300px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-overflowText-border {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 300px;
|
||||||
|
border: solid 2px #ccc;
|
||||||
|
padding: 12px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import React from 'react';
|
|||||||
import { detect } from 'detect-browser';
|
import { detect } from 'detect-browser';
|
||||||
import './DebugReportModal.css';
|
import './DebugReportModal.css';
|
||||||
import { ToolbarButton } from '@ohif/ui';
|
import { ToolbarButton } from '@ohif/ui';
|
||||||
|
import { utils } from '@ohif/core';
|
||||||
|
const { studyMetadataManager } = utils;
|
||||||
|
|
||||||
const DubugReportModal = ({
|
const DubugReportModal = ({
|
||||||
viewports,
|
viewports,
|
||||||
@@ -70,7 +72,7 @@ const DubugReportModal = ({
|
|||||||
body += '== Viewport Layout ==\n';
|
body += '== Viewport Layout ==\n';
|
||||||
body += `Rows\t${numRows}\n`;
|
body += `Rows\t${numRows}\n`;
|
||||||
body += `Columns\t${numColumns}\n\n`;
|
body += `Columns\t${numColumns}\n\n`;
|
||||||
body += '== Viewports ==\n';
|
body += '== SeriesInstanceUIDs ==\n';
|
||||||
|
|
||||||
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
||||||
const vsd = viewportSpecificData[viewportIndex];
|
const vsd = viewportSpecificData[viewportIndex];
|
||||||
@@ -83,6 +85,40 @@ const DubugReportModal = ({
|
|||||||
body += `[${row},${column}]\t${vsd.SeriesInstanceUID}\n`;
|
body += `[${row},${column}]\t${vsd.SeriesInstanceUID}\n`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
body += '== ReferencedSEGSeriesInstanceUIDs ==\n';
|
||||||
|
|
||||||
|
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
||||||
|
const displaySet = viewportSpecificData[viewportIndex];
|
||||||
|
const [
|
||||||
|
referencedDisplaySetsRef,
|
||||||
|
referencedDisplaySetsCount,
|
||||||
|
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'SEG');
|
||||||
|
|
||||||
|
const [row, column] = _viewportIndexToViewportPosition(
|
||||||
|
viewportIndex,
|
||||||
|
numColumns
|
||||||
|
);
|
||||||
|
|
||||||
|
body += `[${row},${column}]\t${referencedDisplaySetsRef}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
body += '== ReferencedRTSTRUCTSeriesInstanceUIDs ==\n';
|
||||||
|
|
||||||
|
Object.keys(viewportSpecificData).forEach(viewportIndex => {
|
||||||
|
const displaySet = viewportSpecificData[viewportIndex];
|
||||||
|
const [
|
||||||
|
referencedDisplaySetsRef,
|
||||||
|
referencedDisplaySetsCount,
|
||||||
|
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'RTSTRUCT');
|
||||||
|
|
||||||
|
const [row, column] = _viewportIndexToViewportPosition(
|
||||||
|
viewportIndex,
|
||||||
|
numColumns
|
||||||
|
);
|
||||||
|
|
||||||
|
body += `[${row},${column}]\t${referencedDisplaySetsRef}\n`;
|
||||||
|
});
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,7 +207,7 @@ const getCurrentStudyUrl = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>URL</td>
|
<td>URL</td>
|
||||||
<td>{window.location.href}</td>
|
<td className="debug-overflowText">{window.location.href}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
@@ -222,6 +258,14 @@ const getLayout = viewports => {
|
|||||||
<th>SeriesInstanceUIDs</th>
|
<th>SeriesInstanceUIDs</th>
|
||||||
</tr>
|
</tr>
|
||||||
{getSeriesInstanceUIDsPerRow(viewports)}
|
{getSeriesInstanceUIDsPerRow(viewports)}
|
||||||
|
<tr>
|
||||||
|
<th>ReferencedSEGSeriesInstanceUIDs</th>
|
||||||
|
</tr>
|
||||||
|
{getReferencedSEGSeriesInstanceUIDsPerRow(viewports)}
|
||||||
|
<tr>
|
||||||
|
<th>ReferencedRTSTRUCTSeriesInstanceUIDs</th>
|
||||||
|
</tr>
|
||||||
|
{getReferencedRTSTRUCTSeriesInstanceUIDsPerRow(viewports)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -256,6 +300,129 @@ const getBrowserInfo = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns DisplaySets that reference the target series, sorted by dateTime
|
||||||
|
*
|
||||||
|
* @param {string} StudyInstanceUID
|
||||||
|
* @param {string} SeriesInstanceUID
|
||||||
|
* @returns Array
|
||||||
|
*/
|
||||||
|
const _getReferencedDisplaysets = (
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
Modality
|
||||||
|
) => {
|
||||||
|
/* Referenced DisplaySets */
|
||||||
|
const studyMetadata = studyMetadataManager.get(StudyInstanceUID);
|
||||||
|
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
|
||||||
|
referencedSeriesInstanceUID: SeriesInstanceUID,
|
||||||
|
Modality,
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Sort */
|
||||||
|
referencedDisplaysets.sort((a, b) => {
|
||||||
|
const aNumber = Number(`${a.SeriesDate}${a.SeriesTime}`);
|
||||||
|
const bNumber = Number(`${b.SeriesDate}${b.SeriesTime}`);
|
||||||
|
return bNumber - aNumber;
|
||||||
|
});
|
||||||
|
|
||||||
|
return referencedDisplaysets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _getReferencedSeriesInstanceUIDsString = (displaySet, Modality) => {
|
||||||
|
const referencedDisplaySets = _getReferencedDisplaysets(
|
||||||
|
displaySet.StudyInstanceUID,
|
||||||
|
displaySet.SeriesInstanceUID,
|
||||||
|
Modality
|
||||||
|
);
|
||||||
|
|
||||||
|
let referencedDisplaySetsRef = 'None';
|
||||||
|
const referencedDisplaySetsCount = referencedDisplaySets.length;
|
||||||
|
if (referencedDisplaySetsCount !== 0) {
|
||||||
|
referencedDisplaySetsRef = '';
|
||||||
|
for (let i = 0; i < referencedDisplaySetsCount - 1; i++) {
|
||||||
|
referencedDisplaySetsRef +=
|
||||||
|
referencedDisplaySets[i].SeriesInstanceUID + ', ';
|
||||||
|
}
|
||||||
|
referencedDisplaySetsRef +=
|
||||||
|
referencedDisplaySets[referencedDisplaySetsCount - 1].SeriesInstanceUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [referencedDisplaySetsRef, referencedDisplaySetsCount];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReferencedSEGSeriesInstanceUIDsPerRow = viewports => {
|
||||||
|
const { viewportSpecificData, numColumns } = viewports;
|
||||||
|
|
||||||
|
// NOTE viewportSpecificData is actually an object with numerical keys.
|
||||||
|
return Object.keys(viewportSpecificData).map(viewportIndex => {
|
||||||
|
const displaySet = viewportSpecificData[viewportIndex];
|
||||||
|
const [
|
||||||
|
referencedDisplaySetsRef,
|
||||||
|
referencedDisplaySetsCount,
|
||||||
|
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'SEG');
|
||||||
|
|
||||||
|
const [row, column] = _viewportIndexToViewportPosition(
|
||||||
|
viewportIndex,
|
||||||
|
numColumns
|
||||||
|
);
|
||||||
|
|
||||||
|
if (referencedDisplaySetsCount > 1) {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>{`[${row},${column}]`}</td>
|
||||||
|
<td className="debug-overflowText-border">
|
||||||
|
{referencedDisplaySetsRef}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>{`[${row},${column}]`}</td>
|
||||||
|
<td className="debug-overflowText">{referencedDisplaySetsRef}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReferencedRTSTRUCTSeriesInstanceUIDsPerRow = viewports => {
|
||||||
|
const { viewportSpecificData, numColumns } = viewports;
|
||||||
|
|
||||||
|
// NOTE viewportSpecificData is actually an object with numerical keys.
|
||||||
|
return Object.keys(viewportSpecificData).map(viewportIndex => {
|
||||||
|
const displaySet = viewportSpecificData[viewportIndex];
|
||||||
|
const [
|
||||||
|
referencedDisplaySetsRef,
|
||||||
|
referencedDisplaySetsCount,
|
||||||
|
] = _getReferencedSeriesInstanceUIDsString(displaySet, 'RTSTRUCT');
|
||||||
|
|
||||||
|
const [row, column] = _viewportIndexToViewportPosition(
|
||||||
|
viewportIndex,
|
||||||
|
numColumns
|
||||||
|
);
|
||||||
|
|
||||||
|
if (referencedDisplaySetsCount > 1) {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>{`[${row},${column}]`}</td>
|
||||||
|
<td className="debug-overflowText-border">
|
||||||
|
{referencedDisplaySetsRef}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>{`[${row},${column}]`}</td>
|
||||||
|
<td className="debug-overflowText">{referencedDisplaySetsRef}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getSeriesInstanceUIDsPerRow = viewports => {
|
const getSeriesInstanceUIDsPerRow = viewports => {
|
||||||
const { viewportSpecificData, numColumns } = viewports;
|
const { viewportSpecificData, numColumns } = viewports;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,30 @@
|
|||||||
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.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.17...@ohif/extension-dicom-html@1.3.18) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.16...@ohif/extension-dicom-html@1.3.17) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.3.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.15...@ohif/extension-dicom-html@1.3.16) (2022-02-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.3.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.14...@ohif/extension-dicom-html@1.3.15) (2022-01-20)
|
## [1.3.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-html@1.3.14...@ohif/extension-dicom-html@1.3.15) (2022-01-20)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-dicom-html
|
**Note:** Version bump only for package @ohif/extension-dicom-html
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-html",
|
"name": "@ohif/extension-dicom-html",
|
||||||
"version": "1.3.15",
|
"version": "1.3.18",
|
||||||
"description": "OHIF extension for rendering structured reports to HTML",
|
"description": "OHIF extension for rendering structured reports to HTML",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"dcmjs": "0.19.2",
|
"dcmjs": "0.19.4",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "^16.8.6"
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ const SOP_CLASS_UIDS = {
|
|||||||
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
|
BASIC_TEXT_SR: '1.2.840.10008.5.1.4.1.1.88.11',
|
||||||
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
|
ENHANCED_SR: '1.2.840.10008.5.1.4.1.1.88.22',
|
||||||
COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33',
|
COMPREHENSIVE_SR: '1.2.840.10008.5.1.4.1.1.88.33',
|
||||||
|
COMPREHENSIVE_3D_SR: '1.2.840.10008.5.1.4.1.1.88.34',
|
||||||
PROCEDURE_LOG_STORAGE: '1.2.840.10008.5.1.4.1.1.88.40',
|
PROCEDURE_LOG_STORAGE: '1.2.840.10008.5.1.4.1.1.88.40',
|
||||||
MAMMOGRAPHY_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.50',
|
MAMMOGRAPHY_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.50',
|
||||||
CHEST_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.65',
|
CHEST_CAD_SR: '1.2.840.10008.5.1.4.1.1.88.65',
|
||||||
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67',
|
X_RAY_RADIATION_DOSE_SR: '1.2.840.10008.5.1.4.1.1.88.67',
|
||||||
ACQUISITION_CONTEXT_SR_STORAGE: '1.2.840.10008.5.1.4.1.1.88.71',
|
ACQUISITION_CONTEXT_SR_STORAGE: '1.2.840.10008.5.1.4.1.1.88.71'
|
||||||
};
|
};
|
||||||
|
|
||||||
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
const sopClassUIDs = Object.values(SOP_CLASS_UIDS);
|
||||||
@@ -46,6 +47,8 @@ const OHIFDicomHtmlSopClassHandler = {
|
|||||||
SeriesTime,
|
SeriesTime,
|
||||||
SeriesNumber,
|
SeriesNumber,
|
||||||
authorizationHeaders,
|
authorizationHeaders,
|
||||||
|
sopClassUids: sopClassUIDs,
|
||||||
|
images: series._instances,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,22 @@
|
|||||||
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.7.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.7...@ohif/extension-dicom-rt@0.7.8) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.7.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.6...@ohif/extension-dicom-rt@0.7.7) (2022-02-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.7.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.5...@ohif/extension-dicom-rt@0.7.6) (2022-01-20)
|
## [0.7.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-rt@0.7.5...@ohif/extension-dicom-rt@0.7.6) (2022-01-20)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
**Note:** Version bump only for package @ohif/extension-dicom-rt
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-rt",
|
"name": "@ohif/extension-dicom-rt",
|
||||||
"version": "0.7.6",
|
"version": "0.7.8",
|
||||||
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
"description": "OHIF extension for rendering DICOM RTSTRUCTs on top of cornerstone images.",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"cornerstone-core": "^2.6.1",
|
"cornerstone-core": "^2.6.1",
|
||||||
"cornerstone-tools": "^6.0.6",
|
"cornerstone-tools": "^6.0.6",
|
||||||
"dcmjs": "0.19.2",
|
"dcmjs": "0.19.4",
|
||||||
"gl-matrix": "^3.3.0",
|
"gl-matrix": "^3.3.0",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
|
|||||||
@@ -3,6 +3,22 @@
|
|||||||
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.7.8](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.7...@ohif/extension-dicom-segmentation@0.7.8) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.7.7](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.6...@ohif/extension-dicom-segmentation@0.7.7) (2022-02-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.7.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.5...@ohif/extension-dicom-segmentation@0.7.6) (2022-01-20)
|
## [0.7.6](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-segmentation@0.7.5...@ohif/extension-dicom-segmentation@0.7.6) (2022-01-20)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
**Note:** Version bump only for package @ohif/extension-dicom-segmentation
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-segmentation",
|
"name": "@ohif/extension-dicom-segmentation",
|
||||||
"version": "0.7.6",
|
"version": "0.7.8",
|
||||||
"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",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"cornerstone-core": "^2.6.1",
|
"cornerstone-core": "^2.6.1",
|
||||||
"cornerstone-tools": "^6.0.6",
|
"cornerstone-tools": "^6.0.6",
|
||||||
"dcmjs": "0.19.2",
|
"dcmjs": "0.19.4",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6"
|
"react-dom": "^16.8.6"
|
||||||
|
|||||||
@@ -150,10 +150,16 @@ export default function getSopClassHandlerModule({ servicesManager }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _parseSeg(arrayBuffer, imageIds) {
|
function _parseSeg(arrayBuffer, imageIds) {
|
||||||
|
const skipOverlapping = false;
|
||||||
|
const tolerance = 1e-2;
|
||||||
|
const cornerstoneToolsVersion = 4;
|
||||||
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
|
return dcmjs.adapters.Cornerstone.Segmentation.generateToolState(
|
||||||
imageIds,
|
imageIds,
|
||||||
arrayBuffer,
|
arrayBuffer,
|
||||||
cornerstone.metaData
|
cornerstone.metaData,
|
||||||
|
skipOverlapping,
|
||||||
|
tolerance,
|
||||||
|
cornerstoneToolsVersion
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,22 @@
|
|||||||
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.2.19](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.18...@ohif/extension-dicom-tag-browser@0.2.19) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.2.18](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.17...@ohif/extension-dicom-tag-browser@0.2.18) (2022-02-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.2.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.16...@ohif/extension-dicom-tag-browser@0.2.17) (2022-01-20)
|
## [0.2.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-tag-browser@0.2.16...@ohif/extension-dicom-tag-browser@0.2.17) (2022-01-20)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
**Note:** Version bump only for package @ohif/extension-dicom-tag-browser
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-dicom-tag-browser",
|
"name": "@ohif/extension-dicom-tag-browser",
|
||||||
"version": "0.2.17",
|
"version": "0.2.19",
|
||||||
"description": "OHIF extension for checking DICOM headers.",
|
"description": "OHIF extension for checking DICOM headers.",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^2.6.0",
|
"@ohif/core": "^2.6.0",
|
||||||
"dcmjs": "0.19.2",
|
"dcmjs": "0.19.4",
|
||||||
"react": "^16.8.6"
|
"react": "^16.8.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
## [1.12.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.15...@ohif/extension-vtk@1.12.16) (2022-02-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.12.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.14...@ohif/extension-vtk@1.12.15) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.12.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.13...@ohif/extension-vtk@1.12.14) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.12.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.12...@ohif/extension-vtk@1.12.13) (2022-02-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.12.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.11...@ohif/extension-vtk@1.12.12) (2022-01-29)
|
## [1.12.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.12.11...@ohif/extension-vtk@1.12.12) (2022-01-29)
|
||||||
|
|
||||||
**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.12.12",
|
"version": "1.12.16",
|
||||||
"description": "OHIF extension for VTK.js",
|
"description": "OHIF extension for VTK.js",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"cornerstone-core": "^2.6.1",
|
"cornerstone-core": "^2.6.1",
|
||||||
"cornerstone-tools": "^6.0.6",
|
"cornerstone-tools": "^6.0.6",
|
||||||
"cornerstone-wado-image-loader": "^4.1.0",
|
"cornerstone-wado-image-loader": "^4.1.0",
|
||||||
"dcmjs": "0.19.2",
|
"dcmjs": "0.19.4",
|
||||||
"dicom-parser": "^1.8.11",
|
"dicom-parser": "^1.8.11",
|
||||||
"i18next": "^17.0.3",
|
"i18next": "^17.0.3",
|
||||||
"i18next-browser-languagedetector": "^3.0.1",
|
"i18next-browser-languagedetector": "^3.0.1",
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
"react-vtkjs-viewport": "^0.14.4"
|
"react-vtkjs-viewport": "^0.14.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ohif/core": "^2.16.8",
|
"@ohif/core": "^2.16.12",
|
||||||
"@ohif/ui": "^1.10.5",
|
"@ohif/ui": "^1.10.6",
|
||||||
"cornerstone-tools": "^6.0.6",
|
"cornerstone-tools": "^6.0.6",
|
||||||
"cornerstone-wado-image-loader": "^4.1.0",
|
"cornerstone-wado-image-loader": "^4.1.0",
|
||||||
"dicom-parser": "^1.8.11",
|
"dicom-parser": "^1.8.11",
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
0 silly argv {
|
||||||
|
0 silly argv _: [ 'run' ],
|
||||||
|
0 silly argv stream: true,
|
||||||
|
0 silly argv lernaVersion: '3.18.3',
|
||||||
|
0 silly argv '$0': 'node_modules/.bin/lerna',
|
||||||
|
0 silly argv script: 'dev:viewer'
|
||||||
|
0 silly argv }
|
||||||
|
1 notice cli v3.18.3
|
||||||
|
2 verbose rootPath /home/davide/Development/OIHF/Viewers
|
||||||
|
3 info versioning independent
|
||||||
|
4 info Executing command in 1 package: "yarn run dev:viewer"
|
||||||
|
5 silly npmRunScript.stream [ 'dev:viewer', [], '@ohif/viewer' ]
|
||||||
|
6 silly getExecOpts /home/davide/Development/OIHF/Viewers/platform/viewer undefined
|
||||||
|
7 error MaxBufferError: stderr maxBuffer exceeded
|
||||||
|
7 error at PassThrough.<anonymous> (/home/davide/Development/OIHF/Viewers/node_modules/get-stream/index.js:41:19)
|
||||||
|
7 error at PassThrough.emit (events.js:412:35)
|
||||||
|
7 error at addChunk (internal/streams/readable.js:290:12)
|
||||||
|
7 error at readableAddChunk (internal/streams/readable.js:261:11)
|
||||||
|
7 error at PassThrough.Readable.push (internal/streams/readable.js:204:10)
|
||||||
|
7 error at PassThrough.Transform.push (internal/streams/transform.js:166:32)
|
||||||
|
7 error at PassThrough.afterTransform (internal/streams/transform.js:101:10)
|
||||||
|
7 error at PassThrough._transform (internal/streams/passthrough.js:46:3)
|
||||||
|
7 error at PassThrough.Transform._read (internal/streams/transform.js:205:10)
|
||||||
@@ -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.16.12](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.11...@ohif/core@2.16.12) (2022-02-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.16.11](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.10...@ohif/core@2.16.11) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.16.10](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.9...@ohif/core@2.16.10) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [2.16.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.8...@ohif/core@2.16.9) (2022-02-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.16.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.7...@ohif/core@2.16.8) (2022-01-29)
|
## [2.16.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.16.7...@ohif/core@2.16.8) (2022-01-29)
|
||||||
|
|
||||||
**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.16.8",
|
"version": "2.16.12",
|
||||||
"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,12 +39,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"ajv": "^6.10.0",
|
"ajv": "^6.10.0",
|
||||||
"dcmjs": "0.19.2",
|
"dcmjs": "0.19.4",
|
||||||
"dicomweb-client": "^0.8.3",
|
"dicomweb-client": "^0.8.3",
|
||||||
"immer": "6.0.2",
|
"immer": "9.0.12",
|
||||||
"isomorphic-base64": "^1.0.2",
|
"isomorphic-base64": "^1.0.2",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"lodash.merge": "^4.6.1",
|
"lodash.merge": "^4.6.1",
|
||||||
|
"mathjs": "^10.1.0",
|
||||||
"mousetrap": "^1.6.3",
|
"mousetrap": "^1.6.3",
|
||||||
"retry": "^0.12.0",
|
"retry": "^0.12.0",
|
||||||
"validate.js": "^0.12.0"
|
"validate.js": "^0.12.0"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
POINT: 'POINT',
|
||||||
|
MULTIPOINT: 'MULTIPOINT',
|
||||||
|
POLYLINE: 'POLYLINE',
|
||||||
|
CIRCLE: 'CIRCLE',
|
||||||
|
ELLIPSE: 'ELLIPSE',
|
||||||
|
POLYGON: 'POLYGON',
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const TOOL_NAMES = {
|
||||||
|
DICOM_SR_DISPLAY_TOOL: 'DICOMSRDisplayTool',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TOOL_NAMES;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export const CodeNameCodeSequenceValues = {
|
||||||
|
ImagingMeasurementReport: '126000',
|
||||||
|
ImageLibrary: '111028',
|
||||||
|
ImagingMeasurements: '126010',
|
||||||
|
MeasurementGroup: '125007',
|
||||||
|
ImageLibraryGroup: '126200',
|
||||||
|
TrackingUniqueIdentifier: '112040',
|
||||||
|
TrackingIdentifier: '112039',
|
||||||
|
Finding: '121071',
|
||||||
|
FindingSite: 'G-C0E3', // SRT
|
||||||
|
CornerstoneFreeText: 'CORNERSTONEFREETEXT', // CST4
|
||||||
|
Score: '246262008',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RELATIONSHIP_TYPE = {
|
||||||
|
INFERRED_FROM: 'INFERRED FROM',
|
||||||
|
SELECTED_FROM: 'SELECTED FROM',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodingSchemeDesignators = {
|
||||||
|
SRT: 'SRT',
|
||||||
|
cornerstoneTools4: 'CST4',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
CodeNameCodeSequenceValues,
|
||||||
|
RELATIONSHIP_TYPE,
|
||||||
|
CodingSchemeDesignators,
|
||||||
|
};
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { ImageSet } from '../../classes';
|
||||||
|
import getMeasurements from './utils/getMeasurements';
|
||||||
|
import getReferencedImagesList from './utils/getReferencedImagesList';
|
||||||
|
import isRehydratable from './utils/isRehydratable';
|
||||||
|
import addMeasurement from './utils/addMeasurement';
|
||||||
|
|
||||||
|
const parseSCOORD3D = ({ servicesManager, displaySets }) => {
|
||||||
|
const { MeasurementService } = servicesManager.services;
|
||||||
|
|
||||||
|
const srDisplaySets = displaySets.filter(ds => ds.Modality === 'SR');
|
||||||
|
|
||||||
|
srDisplaySets.forEach(srDisplaySet => {
|
||||||
|
const firstInstance = srDisplaySet.metadata;
|
||||||
|
if (!firstInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ContentSequence } = firstInstance;
|
||||||
|
|
||||||
|
srDisplaySet.referencedImages = getReferencedImagesList(ContentSequence);
|
||||||
|
srDisplaySet.measurements = getMeasurements(ContentSequence, srDisplaySet);
|
||||||
|
const mappings = MeasurementService.getSourceMappings(
|
||||||
|
'CornerstoneTools',
|
||||||
|
'4'
|
||||||
|
);
|
||||||
|
|
||||||
|
srDisplaySet.isHydrated = false;
|
||||||
|
srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings);
|
||||||
|
srDisplaySet.isLoaded = true;
|
||||||
|
|
||||||
|
const imageDisplaySets = displaySets.filter(
|
||||||
|
ds =>
|
||||||
|
ds.Modality !== 'SR' &&
|
||||||
|
ds.Modality !== 'SEG' &&
|
||||||
|
ds.Modality !== 'RTSTRUCT' &&
|
||||||
|
ds.Modality !== 'RTDOSE'
|
||||||
|
);
|
||||||
|
imageDisplaySets.forEach(imageDisplaySet => {
|
||||||
|
// Check currently added displaySets and add measurements if the sources exist.
|
||||||
|
checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, imageDisplaySet);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIfCanAddMeasurementsToDisplaySet = (
|
||||||
|
srDisplaySet,
|
||||||
|
imageDisplaySet
|
||||||
|
) => {
|
||||||
|
let measurements = srDisplaySet.measurements;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look for image sets.
|
||||||
|
* This also filters out _this_ displaySet, as it is not an image set.
|
||||||
|
*/
|
||||||
|
if (!(imageDisplaySet instanceof ImageSet)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sopClassUIDs, images } = imageDisplaySet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter measurements that references the correct sop class.
|
||||||
|
*/
|
||||||
|
measurements = measurements.filter(measurement => {
|
||||||
|
return measurement.coords.some(coord => {
|
||||||
|
if (coord.ReferencedSOPSequence === undefined) {
|
||||||
|
/** we miss the referenced information. We can compare the annotation SCOORD3D coordinates with
|
||||||
|
* the ImagePatientPosition of the frames. However (WARNING!!!),
|
||||||
|
* if more than a source series is present, this logic can find the wrong frame
|
||||||
|
* (i.e. two source series, with the same frameOfReferenceUID,
|
||||||
|
* that have each a frame with the same ImagePositionPatient of the annotation 3D coordinates)
|
||||||
|
*/
|
||||||
|
for (let i = 0; i < images.length; ++i) {
|
||||||
|
const imageMetadata = images[i].getData().metadata;
|
||||||
|
if (
|
||||||
|
imageMetadata.FrameOfReferenceUID !==
|
||||||
|
coord.ReferencedFrameOfReferenceSequence
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sliceNormal = [0, 0, 0];
|
||||||
|
const orientation = imageMetadata.ImageOrientationPatient;
|
||||||
|
sliceNormal[0] =
|
||||||
|
orientation[1] * orientation[5] - orientation[2] * orientation[4];
|
||||||
|
sliceNormal[1] =
|
||||||
|
orientation[2] * orientation[3] - orientation[0] * orientation[5];
|
||||||
|
sliceNormal[2] =
|
||||||
|
orientation[0] * orientation[4] - orientation[1] * orientation[3];
|
||||||
|
|
||||||
|
let distanceAlongNormal = 0;
|
||||||
|
for (let j = 0; j < 3; ++j) {
|
||||||
|
distanceAlongNormal +=
|
||||||
|
sliceNormal[j] * imageMetadata.ImagePositionPatient[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
// assuming 1 mm tolerance
|
||||||
|
if (Math.abs(distanceAlongNormal - coord.GraphicData[2]) > 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
coord.ReferencedSOPSequence = {
|
||||||
|
ReferencedSOPClassUID: imageMetadata.SOPClassUID,
|
||||||
|
ReferencedSOPInstanceUID: imageMetadata.SOPInstanceUID,
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coord.ReferencedSOPSequence === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sopClassUIDs.includes(
|
||||||
|
coord.ReferencedSOPSequence.ReferencedSOPClassUID
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* New display set doesn't have measurements that references the correct sop class.
|
||||||
|
*/
|
||||||
|
if (measurements.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIds = images.map(i => i.getImageId());
|
||||||
|
const SOPInstanceUIDs = images.map(i => i.SOPInstanceUID);
|
||||||
|
measurements.forEach(measurement => {
|
||||||
|
const { coords } = measurement;
|
||||||
|
|
||||||
|
coords.forEach(coord => {
|
||||||
|
if (coord.ReferencedSOPSequence !== undefined) {
|
||||||
|
const imageIndex = SOPInstanceUIDs.findIndex(
|
||||||
|
SOPInstanceUID =>
|
||||||
|
SOPInstanceUID ===
|
||||||
|
coord.ReferencedSOPSequence.ReferencedSOPInstanceUID
|
||||||
|
);
|
||||||
|
if (imageIndex > -1) {
|
||||||
|
const imageId = imageIds[imageIndex];
|
||||||
|
const imageMetadata = images[imageIndex].getData().metadata;
|
||||||
|
addMeasurement(
|
||||||
|
measurement,
|
||||||
|
imageId,
|
||||||
|
imageMetadata,
|
||||||
|
imageDisplaySet.displaySetInstanceUID
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default parseSCOORD3D;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import csTools from 'cornerstone-tools';
|
||||||
|
import OHIF from '../../../';
|
||||||
|
|
||||||
|
/** Internal imports */
|
||||||
|
import TOOL_NAMES from '../constants/toolNames';
|
||||||
|
import getRenderableData from './getRenderableData';
|
||||||
|
|
||||||
|
const globalImageIdSpecificToolStateManager =
|
||||||
|
csTools.globalImageIdSpecificToolStateManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a measurement to a display set.
|
||||||
|
*
|
||||||
|
* @param {*} measurement
|
||||||
|
* @param {*} imageId
|
||||||
|
* @param {*} displaySetInstanceUID
|
||||||
|
*/
|
||||||
|
export default function addMeasurement(
|
||||||
|
measurement,
|
||||||
|
imageId,
|
||||||
|
imageMetadata,
|
||||||
|
displaySetInstanceUID
|
||||||
|
) {
|
||||||
|
// TODO -> Render rotated ellipse .
|
||||||
|
const toolName = TOOL_NAMES.DICOM_SR_DISPLAY_TOOL;
|
||||||
|
|
||||||
|
const measurementData = {
|
||||||
|
TrackingUniqueIdentifier: measurement.TrackingUniqueIdentifier,
|
||||||
|
renderableData: {},
|
||||||
|
labels: measurement.labels,
|
||||||
|
};
|
||||||
|
|
||||||
|
measurement.coords.forEach(coord => {
|
||||||
|
const { GraphicType, GraphicData, ValueType } = coord;
|
||||||
|
|
||||||
|
if (measurementData.renderableData[GraphicType] === undefined) {
|
||||||
|
measurementData.renderableData[GraphicType] = [];
|
||||||
|
}
|
||||||
|
measurementData.renderableData[GraphicType].push(
|
||||||
|
getRenderableData(GraphicType, GraphicData, ValueType, imageMetadata)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
|
||||||
|
if (toolState[imageId] === undefined) {
|
||||||
|
toolState[imageId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageIdToolState = toolState[imageId];
|
||||||
|
|
||||||
|
// If we don't have tool state for this type of tool, add an empty object
|
||||||
|
if (imageIdToolState[toolName] === undefined) {
|
||||||
|
imageIdToolState[toolName] = {
|
||||||
|
data: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolData = imageIdToolState[toolName];
|
||||||
|
|
||||||
|
measurementData.description = `Read-only annotation`;
|
||||||
|
measurementData.isReadOnly = true;
|
||||||
|
toolData.data.push(measurementData);
|
||||||
|
|
||||||
|
addToMeasurementApi({ measurementData, toolName, imageId });
|
||||||
|
|
||||||
|
measurement.loaded = true;
|
||||||
|
measurement.imageId = imageId;
|
||||||
|
measurement.displaySetInstanceUID = displaySetInstanceUID;
|
||||||
|
|
||||||
|
// Remove the unneeded coord now its processed, but keep the SOPInstanceUID.
|
||||||
|
// NOTE: We assume that each SCOORD in the MeasurementGroup maps onto one frame,
|
||||||
|
// It'd be super werid if it didn't anyway as a SCOORD.
|
||||||
|
measurement.ReferencedSOPInstanceUID =
|
||||||
|
measurement.coords[0].ReferencedSOPSequence.ReferencedSOPInstanceUID;
|
||||||
|
|
||||||
|
return measurement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const addToMeasurementApi = ({ measurementData, toolName, imageId }) => {
|
||||||
|
const measurementApi = OHIF.measurements.MeasurementApi.Instance;
|
||||||
|
|
||||||
|
const toolType = toolName;
|
||||||
|
const collection = measurementApi.tools[toolType];
|
||||||
|
if (!collection) return;
|
||||||
|
if (!measurementData || measurementData.cancelled) return;
|
||||||
|
|
||||||
|
const imageAttributes = OHIF.measurements.getImageAttributes(null, imageId);
|
||||||
|
const measurement = Object.assign({}, measurementData, imageAttributes, {
|
||||||
|
lesionNamingNumber: measurementData.lesionNamingNumber,
|
||||||
|
userId: OHIF.user.getUserId(),
|
||||||
|
toolType,
|
||||||
|
});
|
||||||
|
|
||||||
|
const addedMeasurement = measurementApi.addMeasurement(toolType, measurement);
|
||||||
|
Object.assign(measurementData, addedMeasurement);
|
||||||
|
|
||||||
|
const measurementLabel = OHIF.measurements.getLabel(measurementData);
|
||||||
|
if (measurementLabel) {
|
||||||
|
measurementData.labels = [measurementLabel];
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const getCoordsFromSCOORDOrSCOORD3D = (graphicItem, displaySet) => {
|
||||||
|
const { ValueType, RelationshipType, GraphicType, GraphicData } = graphicItem;
|
||||||
|
|
||||||
|
// if (RelationshipType !== RELATIONSHIP_TYPE.INFERRED_FROM) {
|
||||||
|
// console.warn(
|
||||||
|
// `Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM."`
|
||||||
|
// );
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const coords = { ValueType, GraphicType, GraphicData };
|
||||||
|
|
||||||
|
// ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM'
|
||||||
|
if (ValueType === 'SCOORD') {
|
||||||
|
const { ReferencedSOPSequence } = graphicItem.ContentSequence;
|
||||||
|
coords.ReferencedSOPSequence = ReferencedSOPSequence;
|
||||||
|
} else if (ValueType === 'SCOORD3D') {
|
||||||
|
if (graphicItem.ReferencedFrameOfReferenceUID) {
|
||||||
|
coords.ReferencedFrameOfReferenceSequence = graphicItem.ReferencedFrameOfReferenceUID;
|
||||||
|
} else if (graphicItem.ContentSequence) {
|
||||||
|
const {
|
||||||
|
ReferencedFrameOfReferenceSequence,
|
||||||
|
} = graphicItem.ContentSequence;
|
||||||
|
coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return coords;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getCoordsFromSCOORDOrSCOORD3D;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const getLabelFromMeasuredValueSequence = (
|
||||||
|
ConceptNameCodeSequence,
|
||||||
|
MeasuredValueSequence
|
||||||
|
) => {
|
||||||
|
const { CodeMeaning } = ConceptNameCodeSequence;
|
||||||
|
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
|
||||||
|
const { CodeValue } = MeasurementUnitsCodeSequence;
|
||||||
|
|
||||||
|
const formatedNumericValue = NumericValue
|
||||||
|
? Number(NumericValue).toFixed(1)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: CodeMeaning,
|
||||||
|
value: `${formatedNumericValue} ${CodeValue}`,
|
||||||
|
}; // E.g. Long Axis: 31.0 mm
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getLabelFromMeasuredValueSequence;
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { CodeNameCodeSequenceValues } from '../enums';
|
||||||
|
import getSequenceAsArray from './getSequenceAsArray';
|
||||||
|
import getMergedContentSequencesByTrackingUniqueIdentifiers from './getMergedContentSequencesByTrackingUniqueIdentifiers';
|
||||||
|
import processMeasurement from './processMeasurement';
|
||||||
|
|
||||||
|
const getMeasurements = (
|
||||||
|
ImagingMeasurementReportContentSequence,
|
||||||
|
displaySet
|
||||||
|
) => {
|
||||||
|
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.ImagingMeasurements
|
||||||
|
);
|
||||||
|
|
||||||
|
const MeasurementGroups = getSequenceAsArray(
|
||||||
|
ImagingMeasurements.ContentSequence
|
||||||
|
).filter(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.MeasurementGroup
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedContentSequencesByTrackingUniqueIdentifiers = getMergedContentSequencesByTrackingUniqueIdentifiers(
|
||||||
|
MeasurementGroups
|
||||||
|
);
|
||||||
|
|
||||||
|
let measurements = [];
|
||||||
|
|
||||||
|
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
|
||||||
|
trackingUniqueIdentifier => {
|
||||||
|
const mergedContentSequence =
|
||||||
|
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||||
|
trackingUniqueIdentifier
|
||||||
|
];
|
||||||
|
|
||||||
|
const measurement = processMeasurement(mergedContentSequence, displaySet);
|
||||||
|
if (measurement) {
|
||||||
|
measurements.push(measurement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return measurements;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getMeasurements;
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
import getSequenceAsArray from './getSequenceAsArray';
|
||||||
|
import { CodeNameCodeSequenceValues } from '../enums';
|
||||||
|
|
||||||
|
const getMergedContentSequencesByTrackingUniqueIdentifiers = MeasurementGroups => {
|
||||||
|
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
|
||||||
|
|
||||||
|
MeasurementGroups.forEach(MeasurementGroup => {
|
||||||
|
const ContentSequence = getSequenceAsArray(
|
||||||
|
MeasurementGroup.ContentSequence
|
||||||
|
);
|
||||||
|
|
||||||
|
const TrackingUniqueIdentifierItem = ContentSequence.find(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!TrackingUniqueIdentifierItem) {
|
||||||
|
console.warn(
|
||||||
|
'No Tracking Unique Identifier, skipping ambiguous measurement.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID;
|
||||||
|
|
||||||
|
if (
|
||||||
|
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||||
|
trackingUniqueIdentifier
|
||||||
|
] === undefined
|
||||||
|
) {
|
||||||
|
// Add the full ContentSequence
|
||||||
|
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||||
|
trackingUniqueIdentifier
|
||||||
|
] = [...ContentSequence];
|
||||||
|
} else {
|
||||||
|
// Add the ContentSequence minus the tracking identifier, as we have this
|
||||||
|
// Information in the merged ContentSequence anyway.
|
||||||
|
ContentSequence.forEach(item => {
|
||||||
|
if (
|
||||||
|
item.ConceptNameCodeSequence.CodeValue !==
|
||||||
|
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
|
||||||
|
) {
|
||||||
|
mergedContentSequencesByTrackingUniqueIdentifiers[
|
||||||
|
trackingUniqueIdentifier
|
||||||
|
].push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedContentSequencesByTrackingUniqueIdentifiers;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getMergedContentSequencesByTrackingUniqueIdentifiers;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import getSequenceAsArray from './getSequenceAsArray';
|
||||||
|
import { CodeNameCodeSequenceValues } from '../enums';
|
||||||
|
|
||||||
|
const getReferencedImagesList = ImagingMeasurementReportContentSequence => {
|
||||||
|
const referencedImages = [];
|
||||||
|
|
||||||
|
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.ImageLibrary
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!ImageLibrary.ContentSequence) {
|
||||||
|
return referencedImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageLibraryGroup = getSequenceAsArray(
|
||||||
|
ImageLibrary.ContentSequence
|
||||||
|
).find(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.ImageLibraryGroup
|
||||||
|
);
|
||||||
|
|
||||||
|
getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
||||||
|
const { ReferencedSOPSequence } = item;
|
||||||
|
if (ReferencedSOPSequence) {
|
||||||
|
const {
|
||||||
|
ReferencedSOPClassUID,
|
||||||
|
ReferencedSOPInstanceUID,
|
||||||
|
} = ReferencedSOPSequence;
|
||||||
|
|
||||||
|
referencedImages.push({
|
||||||
|
ReferencedSOPClassUID,
|
||||||
|
ReferencedSOPInstanceUID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return referencedImages;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getReferencedImagesList;
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import csMath from 'cornerstone-math';
|
||||||
|
import SCOORD_TYPES from '../constants/scoordTypes';
|
||||||
|
import { inv } from 'mathjs';
|
||||||
|
|
||||||
|
const getRenderableData = (
|
||||||
|
GraphicType,
|
||||||
|
GraphicData,
|
||||||
|
ValueType,
|
||||||
|
imageMetadata
|
||||||
|
) => {
|
||||||
|
let renderableData;
|
||||||
|
|
||||||
|
const orientation = imageMetadata.ImageOrientationPatient;
|
||||||
|
const position = imageMetadata.ImagePositionPatient;
|
||||||
|
const pixelSpacing = imageMetadata.PixelSpacing;
|
||||||
|
const sliceSpacing = imageMetadata.SliceThickness
|
||||||
|
? imageMetadata.SliceThickness
|
||||||
|
: 1;
|
||||||
|
// https://nipy.org/nibabel/dicom/dicom_orientation.html
|
||||||
|
const M = [
|
||||||
|
[
|
||||||
|
orientation[0] * pixelSpacing[0],
|
||||||
|
orientation[3] * pixelSpacing[1],
|
||||||
|
sliceSpacing,
|
||||||
|
position[0],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
orientation[1] * pixelSpacing[0],
|
||||||
|
orientation[4] * pixelSpacing[1],
|
||||||
|
sliceSpacing,
|
||||||
|
position[1],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
orientation[2] * pixelSpacing[0],
|
||||||
|
orientation[5] * pixelSpacing[1],
|
||||||
|
sliceSpacing,
|
||||||
|
position[2],
|
||||||
|
],
|
||||||
|
[0, 0, 0, 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
// we need to go from 3D to pixel (cornerstone2D works in pixel coordinates),
|
||||||
|
// we take the inverse.
|
||||||
|
const M1 = inv(M);
|
||||||
|
|
||||||
|
const worldToIJK = (point, M1) => {
|
||||||
|
const worldPoint = {
|
||||||
|
x:
|
||||||
|
M1[0][0] * point.x + M1[0][1] * point.y + M1[0][2] * point.z + M1[0][3],
|
||||||
|
y:
|
||||||
|
M1[1][0] * point.x + M1[1][1] * point.y + M1[1][2] * point.z + M1[1][3],
|
||||||
|
z:
|
||||||
|
M1[2][0] * point.x + M1[2][1] * point.y + M1[2][2] * point.z + M1[2][3],
|
||||||
|
};
|
||||||
|
return worldPoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://dicom.innolitics.com/ciods/procedure-log/sr-document-content/00700023
|
||||||
|
switch (GraphicType) {
|
||||||
|
case SCOORD_TYPES.POINT:
|
||||||
|
renderableData = [];
|
||||||
|
|
||||||
|
if (ValueType === 'SCOORD3D') {
|
||||||
|
for (let i = 0; i < GraphicData.length; i += 3) {
|
||||||
|
const point = {
|
||||||
|
x: GraphicData[i],
|
||||||
|
y: GraphicData[i + 1],
|
||||||
|
z: GraphicData[i + 2],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderableData.push(worldToIJK(point, M1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||||
|
renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.MULTIPOINT:
|
||||||
|
renderableData = [];
|
||||||
|
|
||||||
|
if (ValueType === 'SCOORD3D') {
|
||||||
|
for (let i = 0; i < GraphicData.length; i += 3) {
|
||||||
|
const point = {
|
||||||
|
x: GraphicData[i],
|
||||||
|
y: GraphicData[i + 1],
|
||||||
|
z: GraphicData[i + 2],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderableData.push(worldToIJK(point, M1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||||
|
renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.POLYLINE:
|
||||||
|
renderableData = [];
|
||||||
|
|
||||||
|
if (ValueType === 'SCOORD3D') {
|
||||||
|
for (let i = 0; i < GraphicData.length; i += 3) {
|
||||||
|
const point = {
|
||||||
|
x: GraphicData[i],
|
||||||
|
y: GraphicData[i + 1],
|
||||||
|
z: GraphicData[i + 2],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderableData.push(worldToIJK(point, M1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < GraphicData.length; i += 2) {
|
||||||
|
renderableData.push({ x: GraphicData[i], y: GraphicData[i + 1] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.POLYGON:
|
||||||
|
// this is only scoord3d
|
||||||
|
renderableData = [];
|
||||||
|
for (let i = 0; i < GraphicData.length; i += 3) {
|
||||||
|
const point = {
|
||||||
|
x: GraphicData[i],
|
||||||
|
y: GraphicData[i + 1],
|
||||||
|
z: GraphicData[i + 2],
|
||||||
|
};
|
||||||
|
|
||||||
|
renderableData.push(worldToIJK(point, M1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SCOORD_TYPES.CIRCLE: {
|
||||||
|
// this is only scoord
|
||||||
|
const center = { x: GraphicData[0], y: GraphicData[1] };
|
||||||
|
const onPerimeter = { x: GraphicData[2], y: GraphicData[3] };
|
||||||
|
|
||||||
|
const radius = csMath.point.distance(center, onPerimeter);
|
||||||
|
|
||||||
|
renderableData = {
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SCOORD_TYPES.ELLIPSE: {
|
||||||
|
console.warn('ROTATED ELLIPSE NOT YET SUPPORTED!');
|
||||||
|
// To Do: scoord3d ellips, need data for testing
|
||||||
|
const majorAxis = [
|
||||||
|
{ x: GraphicData[0], y: GraphicData[1] },
|
||||||
|
{ x: GraphicData[2], y: GraphicData[3] },
|
||||||
|
];
|
||||||
|
const minorAxis = [
|
||||||
|
{ x: GraphicData[4], y: GraphicData[5] },
|
||||||
|
{ x: GraphicData[6], y: GraphicData[7] },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Calculate two opposite corners of box defined by two axes.
|
||||||
|
|
||||||
|
const minorAxisLength = csMath.point.distance(minorAxis[0], minorAxis[1]);
|
||||||
|
|
||||||
|
const minorAxisDirection = {
|
||||||
|
x: (minorAxis[1].x - minorAxis[0].x) / minorAxisLength,
|
||||||
|
y: (minorAxis[1].y - minorAxis[0].y) / minorAxisLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
const halfMinorAxisLength = minorAxisLength / 2;
|
||||||
|
|
||||||
|
// First end point of major axis + half minor axis vector
|
||||||
|
const corner1 = {
|
||||||
|
x: majorAxis[0].x + minorAxisDirection.x * halfMinorAxisLength,
|
||||||
|
y: majorAxis[0].y + minorAxisDirection.y * halfMinorAxisLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Second end point of major axis - half of minor axis vector
|
||||||
|
const corner2 = {
|
||||||
|
x: majorAxis[1].x - minorAxisDirection.x * halfMinorAxisLength,
|
||||||
|
y: majorAxis[1].y - minorAxisDirection.y * halfMinorAxisLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderableData = {
|
||||||
|
corner1,
|
||||||
|
corner2,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderableData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getRenderableData;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
const getSequenceAsArray = sequence =>
|
||||||
|
Array.isArray(sequence) ? sequence : [sequence];
|
||||||
|
|
||||||
|
export default getSequenceAsArray;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { adapters } from 'dcmjs';
|
||||||
|
|
||||||
|
const cornerstoneAdapters = adapters.Cornerstone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given `displaySet`can be rehydrated into the `MeasurementService`.
|
||||||
|
*
|
||||||
|
* @param {object} displaySet The SR `displaySet` to check.
|
||||||
|
* @param {object[]} mappings The CornerstoneTools 4 mappings to the `MeasurementService`.
|
||||||
|
* @returns {boolean} True if the SR can be rehydrated into the `MeasurementService`.
|
||||||
|
*/
|
||||||
|
export default function isRehydratable(displaySet, mappings) {
|
||||||
|
if (!mappings || !mappings.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mappingDefinitions = mappings.map(m => m.definition);
|
||||||
|
const { measurements } = displaySet;
|
||||||
|
|
||||||
|
const adapterKeys = Object.keys(cornerstoneAdapters).filter(
|
||||||
|
adapterKey =>
|
||||||
|
typeof cornerstoneAdapters[adapterKey]
|
||||||
|
.isValidCornerstoneTrackingIdentifier === 'function'
|
||||||
|
);
|
||||||
|
|
||||||
|
const adapters = [];
|
||||||
|
|
||||||
|
adapterKeys.forEach(key => {
|
||||||
|
if (mappingDefinitions.includes(key)) {
|
||||||
|
// Must have both a dcmjs adapter and a MeasurementService
|
||||||
|
// Definition in order to be a candidate for import.
|
||||||
|
adapters.push(cornerstoneAdapters[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 0; i < measurements.length; i++) {
|
||||||
|
const TrackingIdentifier = measurements[i].TrackingIdentifier;
|
||||||
|
const hydratable = adapters.some(adapter =>
|
||||||
|
adapter.isValidCornerstoneTrackingIdentifier(TrackingIdentifier)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hydratable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import processTID1410Measurement from './processTID1410Measurement';
|
||||||
|
import processNonGeometricallyDefinedMeasurement from './processNonGeometricallyDefinedMeasurement';
|
||||||
|
|
||||||
|
const processMeasurement = (mergedContentSequence, displaySet) => {
|
||||||
|
if (
|
||||||
|
mergedContentSequence.some(
|
||||||
|
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return processTID1410Measurement(mergedContentSequence, displaySet);
|
||||||
|
}
|
||||||
|
|
||||||
|
return processNonGeometricallyDefinedMeasurement(mergedContentSequence);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default processMeasurement;
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
import getLabelFromMeasuredValueSequence from './getLabelFromMeasuredValueSequence';
|
||||||
|
import getCoordsFromSCOORDOrSCOORD3D from './getCoordsFromSCOORDOrSCOORD3D';
|
||||||
|
import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from '../enums';
|
||||||
|
|
||||||
|
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
||||||
|
|
||||||
|
const processNonGeometricallyDefinedMeasurement = mergedContentSequence => {
|
||||||
|
const NUMContentItems = mergedContentSequence.filter(
|
||||||
|
group => group.ValueType === 'NUM'
|
||||||
|
);
|
||||||
|
|
||||||
|
const UIDREFContentItem = mergedContentSequence.find(
|
||||||
|
group => group.ValueType === 'UIDREF'
|
||||||
|
);
|
||||||
|
|
||||||
|
const TrackingIdentifierContentItem = mergedContentSequence.find(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
|
const Finding = mergedContentSequence.find(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.Finding
|
||||||
|
);
|
||||||
|
|
||||||
|
const FindingSites = mergedContentSequence.filter(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
|
||||||
|
CodingSchemeDesignators.SRT &&
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.FindingSite
|
||||||
|
);
|
||||||
|
|
||||||
|
const measurement = {
|
||||||
|
loaded: false,
|
||||||
|
labels: [],
|
||||||
|
coords: [],
|
||||||
|
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
||||||
|
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
Finding &&
|
||||||
|
Finding.ConceptCodeSequence.CodingSchemeDesignator ===
|
||||||
|
CodingSchemeDesignators.cornerstoneTools4 &&
|
||||||
|
Finding.ConceptCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||||
|
) {
|
||||||
|
measurement.labels.push({
|
||||||
|
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||||
|
value: Finding.ConceptCodeSequence.CodeMeaning,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO -> Eventually hopefully support SNOMED or some proper code library, just free text for now.
|
||||||
|
if (FindingSites.length) {
|
||||||
|
const cornerstoneFreeTextFindingSite = FindingSites.find(
|
||||||
|
FindingSite =>
|
||||||
|
FindingSite.ConceptCodeSequence.CodingSchemeDesignator ===
|
||||||
|
CodingSchemeDesignators.cornerstoneTools4 &&
|
||||||
|
FindingSite.ConceptCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.CornerstoneFreeText
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cornerstoneFreeTextFindingSite) {
|
||||||
|
measurement.labels.push({
|
||||||
|
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
||||||
|
value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NUMContentItems.forEach(item => {
|
||||||
|
const {
|
||||||
|
ConceptNameCodeSequence,
|
||||||
|
ContentSequence,
|
||||||
|
MeasuredValueSequence,
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
if (!ContentSequence) {
|
||||||
|
console.warn(`Graphic ${ContentSequence} missing, skipping annotation.`);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ValueType } = ContentSequence;
|
||||||
|
|
||||||
|
if (!ValueType === 'SCOORD' && !ValueType === 'SCOORD3D') {
|
||||||
|
console.warn(
|
||||||
|
`Graphic ${ValueType} not currently supported, skipping annotation.`
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const coords = getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
|
||||||
|
|
||||||
|
if (coords) {
|
||||||
|
measurement.coords.push(coords);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MeasuredValueSequence) {
|
||||||
|
measurement.labels.push(
|
||||||
|
getLabelFromMeasuredValueSequence(
|
||||||
|
ConceptNameCodeSequence,
|
||||||
|
MeasuredValueSequence
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return measurement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default processNonGeometricallyDefinedMeasurement;
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import getLabelFromMeasuredValueSequence from './getLabelFromMeasuredValueSequence';
|
||||||
|
import getCoordsFromSCOORDOrSCOORD3D from './getCoordsFromSCOORDOrSCOORD3D';
|
||||||
|
import { RELATIONSHIP_TYPE, CodeNameCodeSequenceValues } from '../enums';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TID 1410 Planar ROI Measurements and Qualitative Evaluations.
|
||||||
|
*
|
||||||
|
* @param {*} mergedContentSequence
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const processTID1410Measurement = (mergedContentSequence, displaySet) => {
|
||||||
|
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
|
||||||
|
// And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D
|
||||||
|
|
||||||
|
const graphicItem = mergedContentSequence.find(
|
||||||
|
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!graphicItem) {
|
||||||
|
console.warn(
|
||||||
|
`graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIDREFContentItem = mergedContentSequence.find(
|
||||||
|
group => group.ValueType === 'UIDREF'
|
||||||
|
);
|
||||||
|
|
||||||
|
const TrackingIdentifierContentItem = mergedContentSequence.find(
|
||||||
|
item =>
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.TrackingIdentifier
|
||||||
|
);
|
||||||
|
|
||||||
|
const NUMContentItems = mergedContentSequence.filter(
|
||||||
|
group => group.ValueType === 'NUM'
|
||||||
|
);
|
||||||
|
|
||||||
|
const measurement = {
|
||||||
|
loaded: false,
|
||||||
|
labels: [],
|
||||||
|
coords: [getCoordsFromSCOORDOrSCOORD3D(graphicItem, displaySet)],
|
||||||
|
TrackingUniqueIdentifier: UIDREFContentItem.UID,
|
||||||
|
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
NUMContentItems.forEach(item => {
|
||||||
|
const {
|
||||||
|
ConceptNameCodeSequence,
|
||||||
|
ContentSequence,
|
||||||
|
MeasuredValueSequence,
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
if (
|
||||||
|
item.ConceptNameCodeSequence.CodeValue ===
|
||||||
|
CodeNameCodeSequenceValues.Score
|
||||||
|
) {
|
||||||
|
ContentSequence.forEach(item => {
|
||||||
|
if (
|
||||||
|
[
|
||||||
|
RELATIONSHIP_TYPE.SELECTED_FROM,
|
||||||
|
RELATIONSHIP_TYPE.INFERRED_FROM,
|
||||||
|
].includes(item.RelationshipType)
|
||||||
|
) {
|
||||||
|
if (item.ReferencedSOPSequence) {
|
||||||
|
measurement.coords.forEach(coord => {
|
||||||
|
coord.ReferencedSOPSequence = item.ReferencedSOPSequence;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MeasuredValueSequence) {
|
||||||
|
measurement.labels.push(
|
||||||
|
getLabelFromMeasuredValueSequence(
|
||||||
|
ConceptNameCodeSequence,
|
||||||
|
MeasuredValueSequence
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return measurement;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default processTID1410Measurement;
|
||||||
@@ -19,9 +19,10 @@ import findMostRecentStructuredReport from './utils/findMostRecentStructuredRepo
|
|||||||
* Function to be registered into MeasurementAPI to retrieve measurements from DICOM Structured Reports
|
* Function to be registered into MeasurementAPI to retrieve measurements from DICOM Structured Reports
|
||||||
*
|
*
|
||||||
* @param {serverType} server
|
* @param {serverType} server
|
||||||
|
* @param {object} external
|
||||||
* @returns {Promise} Should resolve with OHIF measurementData object
|
* @returns {Promise} Should resolve with OHIF measurementData object
|
||||||
*/
|
*/
|
||||||
const retrieveMeasurements = server => {
|
const retrieveMeasurements = (server, external = {}) => {
|
||||||
log.info('[DICOMSR] retrieveMeasurements');
|
log.info('[DICOMSR] retrieveMeasurements');
|
||||||
|
|
||||||
if (!server || server.type !== 'dicomWeb') {
|
if (!server || server.type !== 'dicomWeb') {
|
||||||
@@ -36,7 +37,7 @@ const retrieveMeasurements = server => {
|
|||||||
|
|
||||||
if (!latestSeries) return Promise.resolve({});
|
if (!latestSeries) return Promise.resolve({});
|
||||||
|
|
||||||
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl);
|
return retrieveMeasurementFromSR(latestSeries, studies, serverUrl, external);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,9 +17,15 @@ const TRANSFER_SYNTAX_UID = '1.2.840.10008.1.2.1';
|
|||||||
* @param {Array} series - List of all series metaData loaded
|
* @param {Array} series - List of all series metaData loaded
|
||||||
* @param {Array} studies - List of all studies metaData loaded
|
* @param {Array} studies - List of all studies metaData loaded
|
||||||
* @param {string} serverUrl - Server URL to be used on request
|
* @param {string} serverUrl - Server URL to be used on request
|
||||||
|
* @param {object} external
|
||||||
* @returns {Object} MeasurementData
|
* @returns {Object} MeasurementData
|
||||||
*/
|
*/
|
||||||
const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
|
const retrieveMeasurementFromSR = async (
|
||||||
|
series,
|
||||||
|
studies,
|
||||||
|
serverUrl,
|
||||||
|
external
|
||||||
|
) => {
|
||||||
const config = {
|
const config = {
|
||||||
url: serverUrl,
|
url: serverUrl,
|
||||||
headers: DICOMWeb.getAuthorizationHeader(),
|
headers: DICOMWeb.getAuthorizationHeader(),
|
||||||
@@ -40,7 +46,8 @@ const retrieveMeasurementFromSR = async (series, studies, serverUrl) => {
|
|||||||
const displaySets = getAllDisplaySets(studies);
|
const displaySets = getAllDisplaySets(studies);
|
||||||
const measurementsData = parseDicomStructuredReport(
|
const measurementsData = parseDicomStructuredReport(
|
||||||
part10SRArrayBuffer,
|
part10SRArrayBuffer,
|
||||||
displaySets
|
displaySets,
|
||||||
|
external
|
||||||
);
|
);
|
||||||
|
|
||||||
return measurementsData;
|
return measurementsData;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import classes from '../classes';
|
import classes from '../classes';
|
||||||
|
import parseSCOORD3D from './SCOORD3D/parseSCOORD3D';
|
||||||
|
|
||||||
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
|
import findInstanceMetadataBySopInstanceUID from './utils/findInstanceMetadataBySopInstanceUid';
|
||||||
|
|
||||||
@@ -12,11 +13,19 @@ const { LogManager } = classes;
|
|||||||
*
|
*
|
||||||
* @param {ArrayBuffer} part10SRArrayBuffer
|
* @param {ArrayBuffer} part10SRArrayBuffer
|
||||||
* @param {Array} displaySets
|
* @param {Array} displaySets
|
||||||
|
* @param {object} external
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const parseDicomStructuredReport = (part10SRArrayBuffer, displaySets) => {
|
const parseDicomStructuredReport = (
|
||||||
// Get the dicom data as an Object
|
part10SRArrayBuffer,
|
||||||
|
displaySets,
|
||||||
|
external
|
||||||
|
) => {
|
||||||
|
if (external && external.servicesManager) {
|
||||||
|
parseSCOORD3D({ servicesManager: external.servicesManager, displaySets });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the dicom data as an Object
|
||||||
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
|
const dicomData = dcmjs.data.DicomMessage.readFile(part10SRArrayBuffer);
|
||||||
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
const dataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
|
||||||
dicomData.dict
|
dicomData.dict
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const isStructuredReportSeries = series => {
|
|||||||
const supportedSopClassUIDs = [
|
const supportedSopClassUIDs = [
|
||||||
'1.2.840.10008.5.1.4.1.1.88.22',
|
'1.2.840.10008.5.1.4.1.1.88.22',
|
||||||
'1.2.840.10008.5.1.4.1.1.11.1',
|
'1.2.840.10008.5.1.4.1.1.11.1',
|
||||||
|
'1.2.840.10008.5.1.4.1.1.88.34', // COMPREHENSIVE_3D_SR
|
||||||
];
|
];
|
||||||
|
|
||||||
const firstInstance = series.getFirstInstance();
|
const firstInstance = series.getFirstInstance();
|
||||||
|
|||||||
@@ -802,6 +802,10 @@ export default class MeasurementApi {
|
|||||||
collection.push(addedMeasurement);
|
collection.push(addedMeasurement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (measurement.isReadOnly) {
|
||||||
|
addedMeasurement.isReadOnly = measurement.isReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
if (!emptyItem) {
|
if (!emptyItem) {
|
||||||
// Reflect the entry in the tool group collection
|
// Reflect the entry in the tool group collection
|
||||||
groupCollection.push({
|
groupCollection.push({
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
|
|
||||||
export default function(element) {
|
export default function(element, imageId) {
|
||||||
// Get the Cornerstone imageId
|
if (!imageId) {
|
||||||
const enabledElement = cornerstone.getEnabledElement(element);
|
// Get the Cornerstone imageId
|
||||||
const imageId = enabledElement.image.imageId;
|
const enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
imageId = enabledElement.image.imageId;
|
||||||
|
}
|
||||||
|
|
||||||
// Get StudyInstanceUID & PatientID
|
// Get StudyInstanceUID & PatientID
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export const dicomSRDisplayTool = {
|
||||||
|
id: 'DICOMSRDisplayTool',
|
||||||
|
name: 'DICOMSRDisplayTool',
|
||||||
|
toolGroup: 'allTools',
|
||||||
|
cornerstoneToolType: 'DICOMSRDisplayTool',
|
||||||
|
options: {
|
||||||
|
measurementTable: {
|
||||||
|
displayFunction: data => {
|
||||||
|
return `(SR) ${data.lesionNamingNumber ||
|
||||||
|
data.measurementNumber ||
|
||||||
|
data.text ||
|
||||||
|
''}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
caseProgress: {
|
||||||
|
include: true,
|
||||||
|
evaluate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default dicomSRDisplayTool;
|
||||||
@@ -10,6 +10,7 @@ import { angle } from './angle';
|
|||||||
import { targetCR } from './targetCR';
|
import { targetCR } from './targetCR';
|
||||||
import { targetNE } from './targetNE';
|
import { targetNE } from './targetNE';
|
||||||
import { targetUN } from './targetUN';
|
import { targetUN } from './targetUN';
|
||||||
|
import dicomSRDisplayTool from './dicomSRDisplayTool';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
arrowAnnotate,
|
arrowAnnotate,
|
||||||
@@ -24,4 +25,5 @@ export {
|
|||||||
targetCR,
|
targetCR,
|
||||||
targetNE,
|
targetNE,
|
||||||
targetUN,
|
targetUN,
|
||||||
|
dicomSRDisplayTool,
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
/** Internal imports */
|
||||||
import log from '../../log';
|
import log from '../../log';
|
||||||
import guid from '../../utils/guid';
|
import guid from '../../utils/guid';
|
||||||
|
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Measurement source schema
|
* Measurement source schema
|
||||||
@@ -15,7 +17,7 @@ import guid from '../../utils/guid';
|
|||||||
*
|
*
|
||||||
* @typedef {Object} Measurement
|
* @typedef {Object} Measurement
|
||||||
* @property {number} id -
|
* @property {number} id -
|
||||||
* @property {string} sopInstanceUid -
|
* @property {string} SOPInstanceUID -
|
||||||
* @property {string} FrameOfReferenceUID -
|
* @property {string} FrameOfReferenceUID -
|
||||||
* @property {string} referenceSeriesUID -
|
* @property {string} referenceSeriesUID -
|
||||||
* @property {string} label -
|
* @property {string} label -
|
||||||
@@ -32,24 +34,37 @@ const MEASUREMENT_SCHEMA_KEYS = [
|
|||||||
'id',
|
'id',
|
||||||
'SOPInstanceUID',
|
'SOPInstanceUID',
|
||||||
'FrameOfReferenceUID',
|
'FrameOfReferenceUID',
|
||||||
|
'referenceStudyUID',
|
||||||
'referenceSeriesUID',
|
'referenceSeriesUID',
|
||||||
'label',
|
'label',
|
||||||
'description',
|
'description',
|
||||||
'type',
|
'type',
|
||||||
'unit',
|
'unit',
|
||||||
'area', // TODO: Add concept names instead (descriptor)
|
'area', // TODO: Add concept names instead (descriptor)
|
||||||
|
'mean',
|
||||||
|
'stdDev',
|
||||||
|
'length',
|
||||||
|
'shortestDiameter',
|
||||||
|
'longestDiameter',
|
||||||
|
'text', // NOTE: There is nothing like this in SR.
|
||||||
'points',
|
'points',
|
||||||
'source',
|
'source',
|
||||||
];
|
];
|
||||||
|
|
||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
MEASUREMENT_UPDATED: 'event::measurement_updated',
|
MEASUREMENT_UPDATED: 'event::measurement_updated',
|
||||||
|
INTERNAL_MEASUREMENT_UPDATED: 'event:internal_measurement_updated',
|
||||||
MEASUREMENT_ADDED: 'event::measurement_added',
|
MEASUREMENT_ADDED: 'event::measurement_added',
|
||||||
|
RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added',
|
||||||
|
MEASUREMENT_REMOVED: 'event::measurement_removed',
|
||||||
|
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
|
||||||
|
JUMP_TO_MEASUREMENT: 'event:jump_to_measurement',
|
||||||
};
|
};
|
||||||
|
|
||||||
const VALUE_TYPES = {
|
const VALUE_TYPES = {
|
||||||
POLYLINE: 'value_type::polyline',
|
POLYLINE: 'value_type::polyline',
|
||||||
POINT: 'value_type::point',
|
POINT: 'value_type::point',
|
||||||
|
BIDIRECTIONAL: 'value_type::shortAxisLongAxis', // TODO -> Discuss with Danny. => just using SCOORD values isn't enough here.
|
||||||
ELLIPSE: 'value_type::ellipse',
|
ELLIPSE: 'value_type::ellipse',
|
||||||
MULTIPOINT: 'value_type::multipoint',
|
MULTIPOINT: 'value_type::multipoint',
|
||||||
CIRCLE: 'value_type::circle',
|
CIRCLE: 'value_type::circle',
|
||||||
@@ -61,6 +76,7 @@ class MeasurementService {
|
|||||||
this.mappings = {};
|
this.mappings = {};
|
||||||
this.measurements = {};
|
this.measurements = {};
|
||||||
this.listeners = {};
|
this.listeners = {};
|
||||||
|
this._jumpToMeasurementCache = {};
|
||||||
Object.defineProperty(this, 'EVENTS', {
|
Object.defineProperty(this, 'EVENTS', {
|
||||||
value: EVENTS,
|
value: EVENTS,
|
||||||
writable: false,
|
writable: false,
|
||||||
@@ -73,6 +89,8 @@ class MeasurementService {
|
|||||||
enumerable: true,
|
enumerable: true,
|
||||||
configurable: false,
|
configurable: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.assign(this, pubSubServiceInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,7 +109,7 @@ class MeasurementService {
|
|||||||
/**
|
/**
|
||||||
* Get specific measurement by its id.
|
* Get specific measurement by its id.
|
||||||
*
|
*
|
||||||
* @param {string} id If of the measurement
|
* @param {string} id Id of the measurement
|
||||||
* @return {Measurement} Measurement instance
|
* @return {Measurement} Measurement instance
|
||||||
*/
|
*/
|
||||||
getMeasurement(id) {
|
getMeasurement(id) {
|
||||||
@@ -114,13 +132,11 @@ class MeasurementService {
|
|||||||
*/
|
*/
|
||||||
createSource(name, version) {
|
createSource(name, version) {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
log.warn('Source name not provided. Exiting early.');
|
throw new Error('Source name not provided.');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!version) {
|
if (!version) {
|
||||||
log.warn('Source version not provided. Exiting early.');
|
throw new Error('Source version not provided.');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = guid();
|
const id = guid();
|
||||||
@@ -132,6 +148,9 @@ class MeasurementService {
|
|||||||
source.addOrUpdate = (definition, measurement) => {
|
source.addOrUpdate = (definition, measurement) => {
|
||||||
return this.addOrUpdate(source, definition, measurement);
|
return this.addOrUpdate(source, definition, measurement);
|
||||||
};
|
};
|
||||||
|
source.remove = id => {
|
||||||
|
return this.remove(id, source);
|
||||||
|
};
|
||||||
source.getAnnotation = (definition, measurementId) => {
|
source.getAnnotation = (definition, measurementId) => {
|
||||||
return this.getAnnotation(source, definition, measurementId);
|
return this.getAnnotation(source, definition, measurementId);
|
||||||
};
|
};
|
||||||
@@ -142,6 +161,32 @@ class MeasurementService {
|
|||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSource(name, version) {
|
||||||
|
const { sources } = this;
|
||||||
|
const id = this._getSourceId(name, version);
|
||||||
|
|
||||||
|
return sources[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
getSourceMappings(name, version) {
|
||||||
|
const { mappings } = this;
|
||||||
|
const id = this._getSourceId(name, version);
|
||||||
|
|
||||||
|
return mappings[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
_getSourceId(name, version) {
|
||||||
|
const { sources } = this;
|
||||||
|
|
||||||
|
const sourceId = Object.keys(sources).find(sourceId => {
|
||||||
|
const source = sources[sourceId];
|
||||||
|
|
||||||
|
return source.name === name && source.version === version;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sourceId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new measurement matching criteria along with mapping functions.
|
* Add a new measurement matching criteria along with mapping functions.
|
||||||
*
|
*
|
||||||
@@ -160,28 +205,23 @@ class MeasurementService {
|
|||||||
toMeasurementSchema
|
toMeasurementSchema
|
||||||
) {
|
) {
|
||||||
if (!this._isValidSource(source)) {
|
if (!this._isValidSource(source)) {
|
||||||
log.warn('Invalid source. Exiting early.');
|
throw new Error('Invalid source.');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!matchingCriteria) {
|
if (!matchingCriteria) {
|
||||||
log.warn('Matching criteria not provided. Exiting early.');
|
throw new Error('Matching criteria not provided.');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!definition) {
|
if (!definition) {
|
||||||
log.warn('Definition not provided. Exiting early.');
|
throw new Error('Definition not provided.');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!toSourceSchema) {
|
if (!toSourceSchema) {
|
||||||
log.warn('Source mapping function not provided. Exiting early.');
|
throw new Error('Mapping function to source schema not provided.');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!toMeasurementSchema) {
|
if (!toMeasurementSchema) {
|
||||||
log.warn('Measurement mapping function not provided. Exiting early.');
|
throw new Error('Measurement mapping function not provided.');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapping = {
|
const mapping = {
|
||||||
@@ -227,9 +267,9 @@ class MeasurementService {
|
|||||||
measurementId,
|
measurementId,
|
||||||
definition
|
definition
|
||||||
);
|
);
|
||||||
|
const measurement = this.getMeasurement(measurementId);
|
||||||
if (mapping) return mapping.toSourceSchema(measurement, definition);
|
if (mapping) return mapping.toSourceSchema(measurement, definition);
|
||||||
|
|
||||||
const measurement = this.getMeasurement(measurementId);
|
|
||||||
const matchingMapping = this._getMatchingMapping(
|
const matchingMapping = this._getMatchingMapping(
|
||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
@@ -243,15 +283,50 @@ class MeasurementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update(id, measurement, notYetUpdatedAtSource = false) {
|
||||||
|
if (this.measurements[id]) {
|
||||||
|
const updatedMeasurement = {
|
||||||
|
...measurement,
|
||||||
|
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`Updating internal measurement representation...`,
|
||||||
|
updatedMeasurement
|
||||||
|
);
|
||||||
|
|
||||||
|
this.measurements[id] = updatedMeasurement;
|
||||||
|
|
||||||
|
this.publish(
|
||||||
|
// Add an internal flag to say the measurement has not yet been updated at source.
|
||||||
|
this.EVENTS.MEASUREMENT_UPDATED,
|
||||||
|
{
|
||||||
|
source: measurement.source,
|
||||||
|
measurement: updatedMeasurement,
|
||||||
|
notYetUpdatedAtSource,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return updatedMeasurement.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds or update persisted measurements.
|
* Add a raw measurement into a source so that it may be
|
||||||
*
|
* Converted to/from annotation in the same way. E.g. import serialized data
|
||||||
* @param {MeasurementSource} source The measurement source instance
|
* Of the same form as the measurement source.
|
||||||
* @param {string} definition The source definition
|
* @param {MeasurementSource} source The measurement source instance.
|
||||||
* @param {Measurement} measurement The source measurement
|
* @param {string} definition The source definition you want to add the measurement to.
|
||||||
* @return {string} A measurement id
|
* @param {object} data The data you wish to add to the source.
|
||||||
|
* @param {function} toMeasurementSchema A function to get the `data` into the same shape as the source definition.
|
||||||
*/
|
*/
|
||||||
addOrUpdate(source, definition, sourceMeasurement) {
|
addRawMeasurement(
|
||||||
|
source,
|
||||||
|
definition,
|
||||||
|
data,
|
||||||
|
toMeasurementSchema,
|
||||||
|
dataSource = {}
|
||||||
|
) {
|
||||||
if (!this._isValidSource(source)) {
|
if (!this._isValidSource(source)) {
|
||||||
log.warn('Invalid source. Exiting early.');
|
log.warn('Invalid source. Exiting early.');
|
||||||
return;
|
return;
|
||||||
@@ -273,14 +348,8 @@ class MeasurementService {
|
|||||||
|
|
||||||
let measurement = {};
|
let measurement = {};
|
||||||
try {
|
try {
|
||||||
const sourceMappings = this.mappings[source.id];
|
|
||||||
const { toMeasurementSchema } = sourceMappings.find(
|
|
||||||
mapping => mapping.definition === definition
|
|
||||||
);
|
|
||||||
|
|
||||||
/* Convert measurement */
|
/* Convert measurement */
|
||||||
measurement = toMeasurementSchema(sourceMeasurement);
|
measurement = toMeasurementSchema(data);
|
||||||
|
|
||||||
/* Assign measurement source instance */
|
/* Assign measurement source instance */
|
||||||
measurement.source = source;
|
measurement.source = source;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -298,7 +367,7 @@ class MeasurementService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let internalId = sourceMeasurement.id;
|
let internalId = data.id;
|
||||||
if (!internalId) {
|
if (!internalId) {
|
||||||
internalId = guid();
|
internalId = guid();
|
||||||
log.warn(`Measurement ID not found. Generating UID: ${internalId}`);
|
log.warn(`Measurement ID not found. Generating UID: ${internalId}`);
|
||||||
@@ -316,49 +385,167 @@ class MeasurementService {
|
|||||||
newMeasurement
|
newMeasurement
|
||||||
);
|
);
|
||||||
this.measurements[internalId] = newMeasurement;
|
this.measurements[internalId] = newMeasurement;
|
||||||
this._broadcastChange(
|
this.publish(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||||
this.EVENTS.MEASUREMENT_UPDATED,
|
|
||||||
source,
|
source,
|
||||||
newMeasurement
|
measurement: newMeasurement,
|
||||||
);
|
});
|
||||||
} else {
|
} else {
|
||||||
log.info(`Measurement added.`, newMeasurement);
|
log.info(`Measurement added.`, newMeasurement);
|
||||||
this.measurements[internalId] = newMeasurement;
|
this.measurements[internalId] = newMeasurement;
|
||||||
this._broadcastChange(
|
this.publish(this.EVENTS.RAW_MEASUREMENT_ADDED, {
|
||||||
this.EVENTS.MEASUREMENT_ADDED,
|
|
||||||
source,
|
source,
|
||||||
newMeasurement
|
measurement: newMeasurement,
|
||||||
);
|
data,
|
||||||
|
dataSource,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return newMeasurement.id;
|
return newMeasurement.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to measurement updates.
|
* Adds or update persisted measurements.
|
||||||
*
|
*
|
||||||
* @param {string} eventName The name of the event
|
* @param {MeasurementSource} source The measurement source instance
|
||||||
* @param {Function} callback Events callback
|
* @param {string} definition The source definition
|
||||||
* @return {Object} Observable object with actions
|
* @param {Measurement} measurement The source measurement
|
||||||
|
* @return {string} A measurement id
|
||||||
*/
|
*/
|
||||||
subscribe(eventName, callback) {
|
addOrUpdate(source, definition, sourceMeasurement) {
|
||||||
if (this._isValidEvent(eventName)) {
|
if (!this._isValidSource(source)) {
|
||||||
const listenerId = guid();
|
throw new Error('Invalid source.');
|
||||||
const subscription = { id: listenerId, callback };
|
|
||||||
|
|
||||||
console.info(`Subscribing to '${eventName}'.`);
|
|
||||||
if (Array.isArray(this.listeners[eventName])) {
|
|
||||||
this.listeners[eventName].push(subscription);
|
|
||||||
} else {
|
|
||||||
this.listeners[eventName] = [subscription];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
unsubscribe: () => this._unsubscribe(eventName, listenerId),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw new Error(`Event ${eventName} not supported.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!definition) {
|
||||||
|
throw new Error('No source definition provided.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceInfo = this._getSourceInfo(source);
|
||||||
|
|
||||||
|
if (!this._sourceHasMappings(source)) {
|
||||||
|
throw new Error(
|
||||||
|
`No measurement mappings found for '${sourceInfo}' source. Exiting early.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let measurement = {};
|
||||||
|
try {
|
||||||
|
const sourceMappings = this.mappings[source.id];
|
||||||
|
const { toMeasurementSchema } = sourceMappings.find(
|
||||||
|
mapping => mapping.definition === definition
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Convert measurement */
|
||||||
|
measurement = toMeasurementSchema(sourceMeasurement);
|
||||||
|
|
||||||
|
/* Assign measurement source instance */
|
||||||
|
measurement.source = source;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to map '${sourceInfo}' measurement for definition ${definition}:`,
|
||||||
|
error.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._isValidMeasurement(measurement)) {
|
||||||
|
throw new Error(
|
||||||
|
`Attempting to add or update a invalid measurement provided by '${sourceInfo}'. Exiting early.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let internalId = sourceMeasurement.id;
|
||||||
|
if (!internalId) {
|
||||||
|
internalId = guid();
|
||||||
|
log.info(`Measurement ID not found. Generating UID: ${internalId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMeasurement = {
|
||||||
|
...measurement,
|
||||||
|
modifiedTimestamp: Math.floor(Date.now() / 1000),
|
||||||
|
id: internalId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.measurements[internalId]) {
|
||||||
|
log.info(
|
||||||
|
`Measurement already defined. Updating measurement.`,
|
||||||
|
newMeasurement
|
||||||
|
);
|
||||||
|
this.measurements[internalId] = newMeasurement;
|
||||||
|
this.publish(this.EVENTS.MEASUREMENT_UPDATED, {
|
||||||
|
source,
|
||||||
|
measurement: newMeasurement,
|
||||||
|
notYetUpdatedAtSource: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.info('Measurement added.', newMeasurement);
|
||||||
|
this.measurements[internalId] = newMeasurement;
|
||||||
|
this.publish(this.EVENTS.MEASUREMENT_ADDED, {
|
||||||
|
source,
|
||||||
|
measurement: newMeasurement,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMeasurement.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a measurement and broadcasts the removed event.
|
||||||
|
*
|
||||||
|
* @param {string} id The measurement id
|
||||||
|
* @param {MeasurementSource} source The measurement source instance
|
||||||
|
* @return {string} The removed measurement id
|
||||||
|
*/
|
||||||
|
remove(id, source) {
|
||||||
|
if (!id || !this.measurements[id]) {
|
||||||
|
log.warn(`No id provided, or unable to find measurement by id.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.measurements[id];
|
||||||
|
this.publish(this.EVENTS.MEASUREMENT_REMOVED, {
|
||||||
|
source,
|
||||||
|
measurement: id, // This is weird :shrug:
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMeasurements() {
|
||||||
|
this.measurements = {};
|
||||||
|
this._jumpToMeasurementCache = {};
|
||||||
|
this.publish(this.EVENTS.MEASUREMENTS_CLEARED);
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpToMeasurement(viewportIndex, id) {
|
||||||
|
const measurement = this.measurements[id];
|
||||||
|
|
||||||
|
if (!measurement) {
|
||||||
|
log.warn(`No id provided, or unable to find measurement by id.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._addJumpToMeasurement(viewportIndex, id);
|
||||||
|
|
||||||
|
const eventName = this.EVENTS.JUMP_TO_MEASUREMENT;
|
||||||
|
|
||||||
|
const hasListeners = Object.keys(this.listeners).length > 0;
|
||||||
|
const hasCallbacks = Array.isArray(this.listeners[eventName]);
|
||||||
|
|
||||||
|
if (hasListeners && hasCallbacks) {
|
||||||
|
this.listeners[eventName].forEach(listener => {
|
||||||
|
listener.callback({ viewportIndex, measurement });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_addJumpToMeasurement(viewportIndex, id) {
|
||||||
|
this._jumpToMeasurementCache[viewportIndex] = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
getJumpToMeasurement(viewportIndex) {
|
||||||
|
return this._jumpToMeasurementCache[viewportIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
removeJumpToMeasurement(viewportIndex) {
|
||||||
|
delete this._jumpToMeasurementCache[viewportIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
_getMappingByMeasurementSource(measurementId, definition) {
|
_getMappingByMeasurementSource(measurementId, definition) {
|
||||||
@@ -370,12 +557,20 @@ class MeasurementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all measurements and broadcasts cleared event.
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.measurements = {};
|
||||||
|
this.publish(this.EVENTS.MEASUREMENTS_CLEARED);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get measurement mapping function if matching criteria.
|
* Get measurement mapping function if matching criteria.
|
||||||
*
|
*
|
||||||
* @param {MeasurementSource} source Measurement source instance
|
* @param {MeasurementSource} source Measurement source instance
|
||||||
* @param {string} definition The source definition
|
* @param {string} definition The source definition
|
||||||
* @param {string} measurement The measurement serice measurement
|
* @param {Measurement} measurement The measurement service measurement
|
||||||
* @return {Object} The mapping based on matched criteria
|
* @return {Object} The mapping based on matched criteria
|
||||||
*/
|
*/
|
||||||
_getMatchingMapping(source, definition, measurement) {
|
_getMatchingMapping(source, definition, measurement) {
|
||||||
@@ -387,10 +582,27 @@ class MeasurementService {
|
|||||||
|
|
||||||
/* Criteria Matching */
|
/* Criteria Matching */
|
||||||
return sourceMappingsByDefinition.find(({ matchingCriteria }) => {
|
return sourceMappingsByDefinition.find(({ matchingCriteria }) => {
|
||||||
return (
|
if (matchingCriteria.type !== measurement.type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
matchingCriteria.properties &&
|
||||||
|
matchingCriteria.properties.every(name =>
|
||||||
|
measurement.hasOwnProperty(name)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
measurement.points &&
|
measurement.points &&
|
||||||
measurement.points.length === matchingCriteria.points
|
measurement.points.length === matchingCriteria.points
|
||||||
);
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,47 +638,6 @@ class MeasurementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcasts measurement changes.
|
|
||||||
*
|
|
||||||
* @param {string} measurementId The measurement id
|
|
||||||
* @param {MeasurementSource} source The measurement source
|
|
||||||
* @param {string} eventName The event name
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
_broadcastChange(eventName, source, measurement) {
|
|
||||||
const hasListeners = Object.keys(this.listeners).length > 0;
|
|
||||||
const hasCallbacks = Array.isArray(this.listeners[eventName]);
|
|
||||||
|
|
||||||
if (hasListeners && hasCallbacks) {
|
|
||||||
this.listeners[eventName].forEach(listener => {
|
|
||||||
listener.callback({ source, measurement });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribe to measurement updates.
|
|
||||||
*
|
|
||||||
* @param {string} eventName The name of the event
|
|
||||||
* @param {string} listenerId The listeners id
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
_unsubscribe(eventName, listenerId) {
|
|
||||||
if (!this.listeners[eventName]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listeners = this.listeners[eventName];
|
|
||||||
if (Array.isArray(listeners)) {
|
|
||||||
this.listeners[eventName] = listeners.filter(
|
|
||||||
({ id }) => id !== listenerId
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.listeners[eventName] = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a given measurement data is valid.
|
* Check if a given measurement data is valid.
|
||||||
*
|
*
|
||||||
@@ -489,10 +660,10 @@ class MeasurementService {
|
|||||||
*
|
*
|
||||||
* @param {string} eventName The name of the event
|
* @param {string} eventName The name of the event
|
||||||
* @return {boolean} Event name validation
|
* @return {boolean} Event name validation
|
||||||
*/
|
// */
|
||||||
_isValidEvent(eventName) {
|
// _isValidEvent(eventName) {
|
||||||
return Object.values(this.EVENTS).includes(eventName);
|
// return Object.values(this.EVENTS).includes(eventName);
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts object of objects to array.
|
* Converts object of objects to array.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ describe('MeasurementService.js', () => {
|
|||||||
let source;
|
let source;
|
||||||
let definition;
|
let definition;
|
||||||
let matchingCriteria;
|
let matchingCriteria;
|
||||||
let toAnnotation;
|
let toSourceSchema;
|
||||||
let toMeasurement;
|
let toMeasurement;
|
||||||
let annotation;
|
let annotation;
|
||||||
|
|
||||||
@@ -34,11 +34,20 @@ describe('MeasurementService.js', () => {
|
|||||||
unit: 'mm',
|
unit: 'mm',
|
||||||
area: 123,
|
area: 123,
|
||||||
type: measurementService.VALUE_TYPES.POLYLINE,
|
type: measurementService.VALUE_TYPES.POLYLINE,
|
||||||
points: [{ x: 1, y: 2 }, { x: 1, y: 2 }],
|
points: [
|
||||||
|
{ x: 1, y: 2 },
|
||||||
|
{ x: 1, y: 2 },
|
||||||
|
],
|
||||||
source: source,
|
source: source,
|
||||||
};
|
};
|
||||||
toAnnotation = () => annotation;
|
toSourceSchema = () => annotation;
|
||||||
toMeasurement = () => measurement;
|
toMeasurement = () => {
|
||||||
|
if (Object.keys(measurement).includes('invalidProperty')) {
|
||||||
|
throw new Error('Measurement does not match schema');
|
||||||
|
}
|
||||||
|
|
||||||
|
return measurement;
|
||||||
|
}
|
||||||
matchingCriteria = {
|
matchingCriteria = {
|
||||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||||
points: 2,
|
points: 2,
|
||||||
@@ -52,16 +61,16 @@ describe('MeasurementService.js', () => {
|
|||||||
measurementService.createSource('Testing', '1');
|
measurementService.createSource('Testing', '1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs warning and return early if no name provided', () => {
|
it('throws Error if no name provided', () => {
|
||||||
measurementService.createSource(null, '1');
|
expect(() => {
|
||||||
|
measurementService.createSource(null, '1')
|
||||||
expect(log.warn.mock.calls.length).toBe(1);
|
}).toThrow(new Error('Source name not provided.'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs warning and return early if no version provided', () => {
|
it('throws Error if no version provided', () => {
|
||||||
measurementService.createSource('Testing', null);
|
expect(() => {
|
||||||
|
measurementService.createSource('Testing', null)
|
||||||
expect(log.warn.mock.calls.length).toBe(1);
|
}).toThrow(new Error('Source version not provided.'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -71,83 +80,84 @@ describe('MeasurementService.js', () => {
|
|||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs warning and return early if no matching criteria provided', () => {
|
it('throws Error if invalid source provided', () => {
|
||||||
measurementService.addMapping(
|
expect(() => {
|
||||||
source,
|
const invalidSource = {};
|
||||||
definition,
|
|
||||||
null,
|
|
||||||
toAnnotation,
|
|
||||||
toMeasurement
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(log.warn.mock.calls.length).toBe(1);
|
measurementService.addMapping(
|
||||||
|
invalidSource,
|
||||||
|
definition,
|
||||||
|
matchingCriteria,
|
||||||
|
toSourceSchema,
|
||||||
|
toMeasurement
|
||||||
|
);
|
||||||
|
}).toThrow(new Error('Invalid source.'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs warning and return early if invalid source provided', () => {
|
it('throws Error if no matching criteria provided', () => {
|
||||||
const invalidSoure = {};
|
expect(() => {
|
||||||
|
measurementService.addMapping(
|
||||||
measurementService.addMapping(
|
source,
|
||||||
invalidSoure,
|
definition,
|
||||||
definition,
|
null,
|
||||||
matchingCriteria,
|
toSourceSchema,
|
||||||
toAnnotation,
|
toMeasurement
|
||||||
toMeasurement
|
);
|
||||||
);
|
}).toThrow(new Error('Matching criteria not provided.'));
|
||||||
|
|
||||||
expect(log.warn.mock.calls.length).toBe(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs warning and return early if no source provided', () => {
|
|
||||||
measurementService.addMapping(
|
|
||||||
null /* source */,
|
|
||||||
definition,
|
|
||||||
matchingCriteria,
|
|
||||||
toAnnotation,
|
|
||||||
toMeasurement
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(log.warn.mock.calls.length).toBe(1);
|
it('throws Error if no source provided', () => {
|
||||||
|
expect(() => {
|
||||||
|
measurementService.addMapping(
|
||||||
|
null /* source */,
|
||||||
|
definition,
|
||||||
|
matchingCriteria,
|
||||||
|
toSourceSchema,
|
||||||
|
toMeasurement
|
||||||
|
);
|
||||||
|
}).toThrow(new Error('Invalid source.'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs warning and return early if no definition provided', () => {
|
it('logs warning and return early if no definition provided', () => {
|
||||||
measurementService.addMapping(
|
expect(() => {
|
||||||
source,
|
measurementService.addMapping(
|
||||||
null /* definition */,
|
source,
|
||||||
matchingCriteria,
|
null /* definition */,
|
||||||
toAnnotation,
|
matchingCriteria,
|
||||||
toMeasurement
|
toSourceSchema,
|
||||||
);
|
toMeasurement
|
||||||
|
);
|
||||||
expect(log.warn.mock.calls.length).toBe(1);
|
}).toThrow(new Error('Definition not provided.'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs warning and return early if no measurement mapping function provided', () => {
|
it('throws Error if no measurement mapping function provided', () => {
|
||||||
measurementService.addMapping(
|
expect(() => {
|
||||||
source,
|
measurementService.addMapping(
|
||||||
definition,
|
source,
|
||||||
matchingCriteria,
|
definition,
|
||||||
null /* toAnnotation */,
|
matchingCriteria,
|
||||||
toMeasurement
|
null /* toSourceSchema */,
|
||||||
);
|
toMeasurement
|
||||||
|
);
|
||||||
expect(log.warn.mock.calls.length).toBe(1);
|
}).toThrow(new Error('Mapping function to source schema not provided.'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs warning and return early if no annotation mapping function provided', () => {
|
it('throws Error if no annotation mapping function provided', () => {
|
||||||
measurementService.addMapping(
|
expect(() => {
|
||||||
source,
|
measurementService.addMapping(
|
||||||
definition,
|
source,
|
||||||
matchingCriteria,
|
definition,
|
||||||
toAnnotation,
|
matchingCriteria,
|
||||||
null /* toMeasurement */
|
toSourceSchema,
|
||||||
);
|
null /* toMeasurement */
|
||||||
|
);
|
||||||
expect(log.warn.mock.calls.length).toBe(1);
|
}).toThrow(new Error('Measurement mapping function not provided.'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,7 +167,7 @@ describe('MeasurementService.js', () => {
|
|||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
const measurementId = source.addOrUpdate(definition, annotation);
|
const measurementId = source.addOrUpdate(definition, annotation);
|
||||||
@@ -171,7 +181,7 @@ describe('MeasurementService.js', () => {
|
|||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
{},
|
{},
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
const measurementId = source.addOrUpdate(definition, annotation);
|
const measurementId = source.addOrUpdate(definition, annotation);
|
||||||
@@ -193,7 +203,7 @@ describe('MeasurementService.js', () => {
|
|||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -212,7 +222,7 @@ describe('MeasurementService.js', () => {
|
|||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -232,7 +242,7 @@ describe('MeasurementService.js', () => {
|
|||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -245,9 +255,9 @@ describe('MeasurementService.js', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('fails to add new measurements when no mapping', () => {
|
it('fails to add new measurements when no mapping', () => {
|
||||||
source.addOrUpdate(definition, measurement);
|
expect(() => {
|
||||||
|
source.addOrUpdate(definition, measurement);
|
||||||
expect(log.warn.mock.calls.length).toBe(1);
|
}).toThrow()
|
||||||
});
|
});
|
||||||
|
|
||||||
it('fails to add new measurements when invalid mapping function', () => {
|
it('fails to add new measurements when invalid mapping function', () => {
|
||||||
@@ -255,13 +265,13 @@ describe('MeasurementService.js', () => {
|
|||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
1 /* Invalid */
|
1 /* Invalid */
|
||||||
);
|
);
|
||||||
|
|
||||||
source.addOrUpdate(definition, measurement);
|
expect(() => {
|
||||||
|
source.addOrUpdate(definition, measurement);
|
||||||
expect(log.warn.mock.calls.length).toBe(1);
|
}).toThrow()
|
||||||
});
|
});
|
||||||
|
|
||||||
it('adds new measurement with custom id', () => {
|
it('adds new measurement with custom id', () => {
|
||||||
@@ -271,7 +281,7 @@ describe('MeasurementService.js', () => {
|
|||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -288,28 +298,28 @@ describe('MeasurementService.js', () => {
|
|||||||
expect(newMeasurement).toEqual(savedMeasurement);
|
expect(newMeasurement).toEqual(savedMeasurement);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('logs warning and return if adding invalid measurement', () => {
|
it('throws Error if adding invalid measurement', () => {
|
||||||
measurement.invalidProperty = {};
|
measurement.invalidProperty = {};
|
||||||
|
|
||||||
measurementService.addMapping(
|
measurementService.addMapping(
|
||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
source.addOrUpdate(definition, measurement);
|
expect(() => {
|
||||||
|
source.addOrUpdate(definition, measurement);
|
||||||
expect(log.warn.mock.calls.length).toBe(2);
|
}).toThrow()
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates existent measurement', () => {
|
it('updates existing measurement', () => {
|
||||||
measurementService.addMapping(
|
measurementService.addMapping(
|
||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -330,7 +340,7 @@ describe('MeasurementService.js', () => {
|
|||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -354,7 +364,7 @@ describe('MeasurementService.js', () => {
|
|||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -381,7 +391,7 @@ describe('MeasurementService.js', () => {
|
|||||||
source,
|
source,
|
||||||
definition,
|
definition,
|
||||||
matchingCriteria,
|
matchingCriteria,
|
||||||
toAnnotation,
|
toSourceSchema,
|
||||||
toMeasurement
|
toMeasurement
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import pubSubServiceInterface from './pubSubServiceInterface';
|
||||||
|
|
||||||
|
export { pubSubServiceInterface };
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import guid from '../../utils/guid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consumer must implement:
|
||||||
|
* this.listeners = {}
|
||||||
|
* this.EVENTS = { "EVENT_KEY": "EVENT_VALUE" }
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
subscribe,
|
||||||
|
publish,
|
||||||
|
_unsubscribe,
|
||||||
|
_isValidEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to updates.
|
||||||
|
*
|
||||||
|
* @param {string} eventName The name of the event
|
||||||
|
* @param {Function} callback Events callback
|
||||||
|
* @return {Object} Observable object with actions
|
||||||
|
*/
|
||||||
|
function subscribe(eventName, callback) {
|
||||||
|
if (this._isValidEvent(eventName)) {
|
||||||
|
const listenerId = guid();
|
||||||
|
const subscription = { id: listenerId, callback };
|
||||||
|
|
||||||
|
console.info(`Subscribing to '${eventName}'.`);
|
||||||
|
if (Array.isArray(this.listeners[eventName])) {
|
||||||
|
this.listeners[eventName].push(subscription);
|
||||||
|
} else {
|
||||||
|
this.listeners[eventName] = [subscription];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
unsubscribe: () => this._unsubscribe(eventName, listenerId),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw new Error(`Event ${eventName} not supported.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe to measurement updates.
|
||||||
|
*
|
||||||
|
* @param {string} eventName The name of the event
|
||||||
|
* @param {string} listenerId The listeners id
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function _unsubscribe(eventName, listenerId) {
|
||||||
|
if (!this.listeners[eventName]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners = this.listeners[eventName];
|
||||||
|
if (Array.isArray(listeners)) {
|
||||||
|
this.listeners[eventName] = listeners.filter(({ id }) => id !== listenerId);
|
||||||
|
} else {
|
||||||
|
this.listeners[eventName] = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a given event is valid.
|
||||||
|
*
|
||||||
|
* @param {string} eventName The name of the event
|
||||||
|
* @return {boolean} Event name validation
|
||||||
|
*/
|
||||||
|
function _isValidEvent(eventName) {
|
||||||
|
return Object.values(this.EVENTS).includes(eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts changes.
|
||||||
|
*
|
||||||
|
* @param {string} eventName - The event name
|
||||||
|
* @param {func} callbackProps - Properties to pass callback
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function publish(eventName, callbackProps) {
|
||||||
|
const hasListeners = Object.keys(this.listeners).length > 0;
|
||||||
|
const hasCallbacks = Array.isArray(this.listeners[eventName]);
|
||||||
|
|
||||||
|
if (hasListeners && hasCallbacks) {
|
||||||
|
this.listeners[eventName].forEach(listener => {
|
||||||
|
listener.callback(callbackProps);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
## [1.10.6](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.5...@ohif/ui@1.10.6) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/ui
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.10.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.4...@ohif/ui@1.10.5) (2021-11-30)
|
## [1.10.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.10.4...@ohif/ui@1.10.5) (2021-11-30)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/ui
|
**Note:** Version bump only for package @ohif/ui
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/ui",
|
"name": "@ohif/ui",
|
||||||
"version": "1.10.5",
|
"version": "1.10.6",
|
||||||
"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",
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ class MeasurementTable extends Component {
|
|||||||
const itemIndex = measurement.itemNumber || index + 1;
|
const itemIndex = measurement.itemNumber || index + 1;
|
||||||
const itemClass =
|
const itemClass =
|
||||||
selectedKey === key && !this.props.readOnly ? 'selected' : '';
|
selectedKey === key && !this.props.readOnly ? 'selected' : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MeasurementTableItem
|
<MeasurementTableItem
|
||||||
key={key}
|
key={key}
|
||||||
|
|||||||
@@ -22,11 +22,15 @@ class MeasurementTableItem extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { warningTitle = '', hasWarnings } = this.props.measurementData;
|
const {
|
||||||
|
warningTitle = '',
|
||||||
|
hasWarnings,
|
||||||
|
isReadOnly,
|
||||||
|
} = this.props.measurementData;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{hasWarnings ? (
|
{hasWarnings && !isReadOnly ? (
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
key={this.props.itemIndex}
|
key={this.props.itemIndex}
|
||||||
placement="left"
|
placement="left"
|
||||||
@@ -44,8 +48,8 @@ class MeasurementTableItem extends Component {
|
|||||||
<div>{this.getTableListItem()}</div>
|
<div>{this.getTableListItem()}</div>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment>{this.getTableListItem()}</React.Fragment>
|
<React.Fragment>{this.getTableListItem()}</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -62,9 +66,11 @@ class MeasurementTableItem extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getTableListItem = () => {
|
getTableListItem = () => {
|
||||||
const hasWarningClass = this.props.measurementData.hasWarnings
|
const hasWarningClass =
|
||||||
? 'hasWarnings'
|
this.props.measurementData.hasWarnings &&
|
||||||
: '';
|
!this.props.measurementData.isReadOnly
|
||||||
|
? 'hasWarnings'
|
||||||
|
: '';
|
||||||
|
|
||||||
const actionButtons = [];
|
const actionButtons = [];
|
||||||
|
|
||||||
@@ -102,7 +108,9 @@ class MeasurementTableItem extends Component {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="displayTexts">{this.getDataDisplayText()}</div>
|
<div className="displayTexts">{this.getDataDisplayText()}</div>
|
||||||
<div className="rowActions">{actionButtons}</div>
|
{!this.props.measurementData.isReadOnly && (
|
||||||
|
<div className="rowActions">{actionButtons}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableListItem>
|
</TableListItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ function ImageThumbnail(props) {
|
|||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error.isCanceled) return;
|
if (error.isCanceled) return;
|
||||||
setLoading(false);
|
// setLoading(false);
|
||||||
setError(true);
|
// setError(true);
|
||||||
throw new Error(error);
|
// throw new Error(error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ services:
|
|||||||
ohif_viewer:
|
ohif_viewer:
|
||||||
build:
|
build:
|
||||||
# Project root
|
# Project root
|
||||||
context: ./../../
|
context: ./../../../../
|
||||||
# Relative to context
|
# Relative to context
|
||||||
dockerfile: ./docker/OpenResty-Orthanc-Keycloak/dockerfile
|
dockerfile: ./platform/viewer/.recipes/OpenResty-Orthanc-Keycloak/dockerfile
|
||||||
image: webapp:latest
|
image: webapp:latest
|
||||||
container_name: webapp
|
container_name: webapp
|
||||||
volumes:
|
volumes:
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
# TODO: Update to use Postgres
|
# TODO: Update to use Postgres
|
||||||
# https://github.com/mrts/docker-postgresql-multiple-databases
|
# https://github.com/mrts/docker-postgresql-multiple-databases
|
||||||
orthanc:
|
orthanc:
|
||||||
image: jodogne/orthanc-plugins:1.5.6
|
image: jodogne/orthanc-plugins:1.9.7
|
||||||
hostname: orthanc
|
hostname: orthanc
|
||||||
container_name: orthanc
|
container_name: orthanc
|
||||||
volumes:
|
volumes:
|
||||||
@@ -47,7 +47,7 @@ services:
|
|||||||
|
|
||||||
# LINK: https://hub.docker.com/r/jboss/keycloak
|
# LINK: https://hub.docker.com/r/jboss/keycloak
|
||||||
keycloak:
|
keycloak:
|
||||||
image: jboss/keycloak:6.0.1
|
image: jboss/keycloak:16.1.0
|
||||||
hostname: keycloak
|
hostname: keycloak
|
||||||
container_name: keycloak
|
container_name: keycloak
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -23,7 +23,9 @@
|
|||||||
|
|
||||||
|
|
||||||
# Stage 1: Build the application
|
# Stage 1: Build the application
|
||||||
FROM node:11.2.0-slim as builder
|
|
||||||
|
# Needs to be pinned at 16.x for now because of https://github.com/webpack/webpack/issues/14532
|
||||||
|
FROM node:lts-slim as builder
|
||||||
|
|
||||||
RUN mkdir /usr/src/app
|
RUN mkdir /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@@ -36,12 +38,12 @@ COPY yarn.lock /usr/src/app/yarn.lock
|
|||||||
|
|
||||||
ADD . /usr/src/app/
|
ADD . /usr/src/app/
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
RUN yarn run build:web
|
RUN yarn run build
|
||||||
|
|
||||||
# Stage 2: Bundle the built application into a Docker container
|
# Stage 2: Bundle the built application into a Docker container
|
||||||
# which runs openresty (nginx) using Alpine Linux
|
# which runs openresty (nginx) using Alpine Linux
|
||||||
# LINK: https://hub.docker.com/r/openresty/openresty
|
# LINK: https://hub.docker.com/r/openresty/openresty
|
||||||
FROM openresty/openresty:1.15.8.1rc1-0-alpine-fat
|
FROM openresty/openresty:1.19.9.1-4-alpine-fat
|
||||||
|
|
||||||
RUN mkdir /var/log/nginx
|
RUN mkdir /var/log/nginx
|
||||||
RUN apk add --no-cache openssl
|
RUN apk add --no-cache openssl
|
||||||
@@ -60,6 +62,6 @@ RUN luarocks install lua-resty-openidc
|
|||||||
RUN luarocks install luacrypto
|
RUN luarocks install luacrypto
|
||||||
|
|
||||||
# Copy build output to image
|
# Copy build output to image
|
||||||
COPY --from=builder /usr/src/app/build /var/www/html
|
COPY --from=builder /usr/src/app/platform/viewer/dist /var/www/html
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/openresty/nginx/sbin/nginx", "-g", "daemon off;"]
|
ENTRYPOINT ["/usr/local/openresty/nginx/sbin/nginx", "-g", "daemon off;"]
|
||||||
@@ -10,9 +10,9 @@ services:
|
|||||||
ohif_viewer:
|
ohif_viewer:
|
||||||
build:
|
build:
|
||||||
# Project root
|
# Project root
|
||||||
context: ./../../
|
context: ./../../../../
|
||||||
# Relative to context
|
# Relative to context
|
||||||
dockerfile: ./docker/OpenResty-Orthanc/dockerfile
|
dockerfile: ./platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
|
||||||
image: webapp:latest
|
image: webapp:latest
|
||||||
container_name: webapp
|
container_name: webapp
|
||||||
volumes:
|
volumes:
|
||||||
@@ -34,7 +34,7 @@ services:
|
|||||||
# TODO: Update to use Postgres
|
# TODO: Update to use Postgres
|
||||||
# https://github.com/mrts/docker-postgresql-multiple-databases
|
# https://github.com/mrts/docker-postgresql-multiple-databases
|
||||||
orthanc:
|
orthanc:
|
||||||
image: jodogne/orthanc-plugins:1.5.6
|
image: jodogne/orthanc-plugins:1.9.7
|
||||||
hostname: orthanc
|
hostname: orthanc
|
||||||
container_name: orthanc
|
container_name: orthanc
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -23,7 +23,9 @@
|
|||||||
|
|
||||||
|
|
||||||
# Stage 1: Build the application
|
# Stage 1: Build the application
|
||||||
FROM node:11.2.0-slim as builder
|
|
||||||
|
# Needs to be pinned at 16.x for now because of https://github.com/webpack/webpack/issues/14532
|
||||||
|
FROM node:lts-slim as builder
|
||||||
|
|
||||||
RUN mkdir /usr/src/app
|
RUN mkdir /usr/src/app
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@@ -36,12 +38,12 @@ COPY yarn.lock /usr/src/app/yarn.lock
|
|||||||
|
|
||||||
ADD . /usr/src/app/
|
ADD . /usr/src/app/
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
RUN yarn run build:web
|
RUN yarn run build
|
||||||
|
|
||||||
# Stage 2: Bundle the built application into a Docker container
|
# Stage 2: Bundle the built application into a Docker container
|
||||||
# which runs openresty (nginx) using Alpine Linux
|
# which runs openresty (nginx) using Alpine Linux
|
||||||
# LINK: https://hub.docker.com/r/openresty/openresty
|
# LINK: https://hub.docker.com/r/openresty/openresty
|
||||||
FROM openresty/openresty:1.15.8.1rc1-0-alpine-fat
|
FROM openresty/openresty:1.19.9.1-4-alpine-fat
|
||||||
|
|
||||||
RUN mkdir /var/log/nginx
|
RUN mkdir /var/log/nginx
|
||||||
RUN apk add --no-cache openssl
|
RUN apk add --no-cache openssl
|
||||||
@@ -60,6 +62,6 @@ RUN luarocks install lua-resty-openidc
|
|||||||
RUN luarocks install luacrypto
|
RUN luarocks install luacrypto
|
||||||
|
|
||||||
# Copy build output to image
|
# Copy build output to image
|
||||||
COPY --from=builder /usr/src/app/build /var/www/html
|
COPY --from=builder /usr/src/app/platform/viewer/dist /var/www/html
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/openresty/nginx/sbin/nginx", "-g", "daemon off;"]
|
ENTRYPOINT ["/usr/local/openresty/nginx/sbin/nginx", "-g", "daemon off;"]
|
||||||
@@ -3,6 +3,54 @@
|
|||||||
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.
|
||||||
|
|
||||||
|
## [4.12.19](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.18...@ohif/viewer@4.12.19) (2022-03-10)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [4.12.18](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.17...@ohif/viewer@4.12.18) (2022-02-15)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [4.12.17](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.16...@ohif/viewer@4.12.17) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [4.12.16](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.15...@ohif/viewer@4.12.16) (2022-02-03)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [4.12.15](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.14...@ohif/viewer@4.12.15) (2022-02-02)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [4.12.14](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.13...@ohif/viewer@4.12.14) (2022-01-31)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [4.12.13](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.12...@ohif/viewer@4.12.13) (2022-01-29)
|
## [4.12.13](https://github.com/OHIF/Viewers/compare/@ohif/viewer@4.12.12...@ohif/viewer@4.12.13) (2022-01-29)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/viewer
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/viewer",
|
"name": "@ohif/viewer",
|
||||||
"version": "4.12.13",
|
"version": "4.12.19",
|
||||||
"description": "OHIF Viewer",
|
"description": "OHIF Viewer",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -47,18 +47,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"@ohif/core": "^2.16.8",
|
"@ohif/core": "^2.16.12",
|
||||||
"@ohif/extension-cornerstone": "^2.12.4",
|
"@ohif/extension-cornerstone": "^2.12.7",
|
||||||
"@ohif/extension-debugging": "^0.3.1",
|
"@ohif/extension-debugging": "^0.3.2",
|
||||||
"@ohif/extension-dicom-html": "^1.3.15",
|
"@ohif/extension-dicom-html": "^1.3.18",
|
||||||
"@ohif/extension-dicom-microscopy": "^0.52.0",
|
"@ohif/extension-dicom-microscopy": "^0.52.0",
|
||||||
"@ohif/extension-dicom-pdf": "^1.1.2",
|
"@ohif/extension-dicom-pdf": "^1.1.2",
|
||||||
"@ohif/extension-dicom-rt": "^0.7.6",
|
"@ohif/extension-dicom-rt": "^0.7.8",
|
||||||
"@ohif/extension-dicom-segmentation": "^0.7.6",
|
"@ohif/extension-dicom-segmentation": "^0.7.8",
|
||||||
"@ohif/extension-lesion-tracker": "^0.2.1",
|
"@ohif/extension-lesion-tracker": "^0.2.1",
|
||||||
"@ohif/extension-vtk": "^1.12.12",
|
"@ohif/extension-vtk": "^1.12.16",
|
||||||
"@ohif/i18n": "^0.53.0",
|
"@ohif/i18n": "^0.53.0",
|
||||||
"@ohif/ui": "^1.10.5",
|
"@ohif/ui": "^1.10.6",
|
||||||
"@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",
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
"cornerstone-math": "^0.1.9",
|
"cornerstone-math": "^0.1.9",
|
||||||
"cornerstone-tools": "^6.0.6",
|
"cornerstone-tools": "^6.0.6",
|
||||||
"cornerstone-wado-image-loader": "^4.1.0",
|
"cornerstone-wado-image-loader": "^4.1.0",
|
||||||
"dcmjs": "0.19.2",
|
"dcmjs": "0.19.4",
|
||||||
"dicom-parser": "^1.8.11",
|
"dicom-parser": "^1.8.11",
|
||||||
"dicomweb-client": "^0.8.3",
|
"dicomweb-client": "^0.8.3",
|
||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
"cypress": "^6.0.0",
|
"cypress": "^6.0.0",
|
||||||
"gh-pages": "2.0.1",
|
"gh-pages": "2.0.1",
|
||||||
"identity-obj-proxy": "3.0.x",
|
"identity-obj-proxy": "3.0.x",
|
||||||
"lodash": "4.17.15",
|
"lodash": "4.17.21",
|
||||||
"lodash.clonedeep": "4.5.0"
|
"lodash.clonedeep": "4.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,6 +136,7 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
|
|||||||
measurementNumber,
|
measurementNumber,
|
||||||
lesionNamingNumber,
|
lesionNamingNumber,
|
||||||
toolType,
|
toolType,
|
||||||
|
isReadOnly
|
||||||
} = measurementData;
|
} = measurementData;
|
||||||
const measurementId = measurementData._id;
|
const measurementId = measurementData._id;
|
||||||
|
|
||||||
@@ -154,6 +155,7 @@ function convertMeasurementsToTableData(toolCollections, timepoints) {
|
|||||||
lesionNamingNumber,
|
lesionNamingNumber,
|
||||||
toolType,
|
toolType,
|
||||||
hasWarnings,
|
hasWarnings,
|
||||||
|
isReadOnly,
|
||||||
warningTitle,
|
warningTitle,
|
||||||
warningList,
|
warningList,
|
||||||
isSplitLesion: false, //TODO
|
isSplitLesion: false, //TODO
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import ConnectedStudyBrowser from './ConnectedStudyBrowser.js';
|
|||||||
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
import ConnectedViewerMain from './ConnectedViewerMain.js';
|
||||||
import SidePanel from './../components/SidePanel.js';
|
import SidePanel from './../components/SidePanel.js';
|
||||||
import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog';
|
import ErrorBoundaryDialog from './../components/ErrorBoundaryDialog';
|
||||||
import { extensionManager } from './../App.js';
|
import { extensionManager, servicesManager } from './../App.js';
|
||||||
import { ReconstructionIssues } from './../../../core/src/enums.js';
|
import { ReconstructionIssues } from './../../../core/src/enums.js';
|
||||||
|
|
||||||
// Contexts
|
// Contexts
|
||||||
@@ -70,9 +70,11 @@ class Viewer extends Component {
|
|||||||
const { activeServer } = this.props;
|
const { activeServer } = this.props;
|
||||||
const server = Object.assign({}, activeServer);
|
const server = Object.assign({}, activeServer);
|
||||||
|
|
||||||
|
const external = { servicesManager };
|
||||||
|
|
||||||
OHIF.measurements.MeasurementApi.setConfiguration({
|
OHIF.measurements.MeasurementApi.setConfiguration({
|
||||||
dataExchange: {
|
dataExchange: {
|
||||||
retrieve: DICOMSR.retrieveMeasurements,
|
retrieve: server => DICOMSR.retrieveMeasurements(server, external),
|
||||||
store: DICOMSR.storeMeasurements,
|
store: DICOMSR.storeMeasurements,
|
||||||
},
|
},
|
||||||
server,
|
server,
|
||||||
|
|||||||
@@ -1247,7 +1247,7 @@
|
|||||||
pirates "^4.0.0"
|
pirates "^4.0.0"
|
||||||
source-map-support "^0.5.9"
|
source-map-support "^0.5.9"
|
||||||
|
|
||||||
"@babel/runtime@7.1.2", "@babel/runtime@7.5.5", "@babel/runtime@7.6.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.6":
|
"@babel/runtime@7.1.2", "@babel/runtime@7.5.5", "@babel/runtime@7.6.0", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.16.5", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.6":
|
||||||
version "7.5.5"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
|
||||||
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
|
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
|
||||||
@@ -5969,6 +5969,11 @@ compare-func@^1.3.1:
|
|||||||
array-ify "^1.0.0"
|
array-ify "^1.0.0"
|
||||||
dot-prop "^3.0.0"
|
dot-prop "^3.0.0"
|
||||||
|
|
||||||
|
complex.js@^2.0.15:
|
||||||
|
version "2.0.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.15.tgz#7add6848b4c1d12aa9262f7df925ebe7a51a7406"
|
||||||
|
integrity sha512-gDBvQU8IG139ZBQTSo2qvDFP+lANMGluM779csXOr6ny1NUtA3wkUnCFjlDNH/moAVfXtvClYt6G0zarFbtz5w==
|
||||||
|
|
||||||
component-emitter@^1.2.1:
|
component-emitter@^1.2.1:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
|
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
|
||||||
@@ -6937,10 +6942,10 @@ dateformat@^3.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
||||||
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
|
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
|
||||||
|
|
||||||
dcmjs@0.19.2:
|
dcmjs@0.19.4:
|
||||||
version "0.19.2"
|
version "0.19.4"
|
||||||
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.19.2.tgz#f3ff81797b17cdb2663dc8212f8a3d7dfd145ae2"
|
resolved "https://registry.yarnpkg.com/dcmjs/-/dcmjs-0.19.4.tgz#b2ce1cc1e672ead3fba12bafd2e8a655b398601a"
|
||||||
integrity sha512-FXdKHnCoaONbgZFwxchpXUhLHOtSdIWGtG2A0UDS72FmptHDDXQH017j1e5aVm0Q5ee+42c+W+xWwBScmSyE7w==
|
integrity sha512-ZpOsuyBMcmDVPo0EtqztRGu1ARKMqIfbJGbtDxmBwxeE5SEHMQJvrZHhyLoxuS10R3w+jL2SJ0r7Dn9aSDZH1w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/polyfill" "^7.8.3"
|
"@babel/polyfill" "^7.8.3"
|
||||||
"@babel/runtime" "^7.8.4"
|
"@babel/runtime" "^7.8.4"
|
||||||
@@ -7009,6 +7014,11 @@ decamelize@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
xregexp "4.0.0"
|
xregexp "4.0.0"
|
||||||
|
|
||||||
|
decimal.js@^10.3.1:
|
||||||
|
version "10.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
|
||||||
|
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
|
||||||
|
|
||||||
decode-uri-component@^0.2.0:
|
decode-uri-component@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||||
@@ -8088,6 +8098,11 @@ escape-html@^1.0.3, escape-html@~1.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||||
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
||||||
|
|
||||||
|
escape-latex@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1"
|
||||||
|
integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==
|
||||||
|
|
||||||
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||||
@@ -9212,6 +9227,11 @@ frac@~1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b"
|
resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b"
|
||||||
integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
|
integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
|
||||||
|
|
||||||
|
fraction.js@^4.1.2:
|
||||||
|
version "4.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.1.2.tgz#13e420a92422b6cf244dff8690ed89401029fbe8"
|
||||||
|
integrity sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==
|
||||||
|
|
||||||
fragment-cache@^0.2.1:
|
fragment-cache@^0.2.1:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
|
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
|
||||||
@@ -10640,10 +10660,10 @@ immer@1.10.0:
|
|||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
|
||||||
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
|
integrity sha512-O3sR1/opvCDGLEVcvrGTMtLac8GJ5IwZC4puPrLuRj3l7ICKvkmA0vGuU9OW8mV9WIBRnaxp5GJh9IEAaNOoYg==
|
||||||
|
|
||||||
immer@6.0.2:
|
immer@9.0.12:
|
||||||
version "6.0.2"
|
version "9.0.12"
|
||||||
resolved "https://registry.yarnpkg.com/immer/-/immer-6.0.2.tgz#5bc08dc4930c756d0749533a2afbd88c8de0cd19"
|
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20"
|
||||||
integrity sha512-56CMvUMZl4kkWJFFUe1TjBgGbyb9ibzpLyHD+RSKSVdytuDXgT/HXO1S+GJVywMVl5neGTdAogoR15eRVEd10Q==
|
integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==
|
||||||
|
|
||||||
immutable@>=3.6.0:
|
immutable@>=3.6.0:
|
||||||
version "3.8.2"
|
version "3.8.2"
|
||||||
@@ -11544,6 +11564,11 @@ istanbul-reports@^2.2.6:
|
|||||||
dependencies:
|
dependencies:
|
||||||
handlebars "^4.1.2"
|
handlebars "^4.1.2"
|
||||||
|
|
||||||
|
javascript-natural-sort@^0.7.1:
|
||||||
|
version "0.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59"
|
||||||
|
integrity sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k=
|
||||||
|
|
||||||
javascript-stringify@^1.6.0:
|
javascript-stringify@^1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3"
|
resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-1.6.0.tgz#142d111f3a6e3dae8f4a9afd77d45855b5a9cce3"
|
||||||
@@ -12816,7 +12841,12 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||||
|
|
||||||
lodash@4.17.15, "lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.1.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10:
|
lodash@4.17.21:
|
||||||
|
version "4.17.21"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
|
"lodash@>=3.5 <5", lodash@^4.0.0, lodash@^4.1.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@~4.17.10:
|
||||||
version "4.17.15"
|
version "4.17.15"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||||
@@ -13110,6 +13140,21 @@ match-sorter@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
remove-accents "0.4.2"
|
remove-accents "0.4.2"
|
||||||
|
|
||||||
|
mathjs@^10.1.0:
|
||||||
|
version "10.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-10.1.0.tgz#87e3e5ec73d9d36bd5d72731ab6d7a10d14f5b32"
|
||||||
|
integrity sha512-TrpZAR3H9jR0Cv6cnzT+TZhE40Xs2SCaLf/qm2WcWm2tui69Gas/bC/ct5ZLZNWnWvNvJ7H2uHvuRDDl151PHA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.16.5"
|
||||||
|
complex.js "^2.0.15"
|
||||||
|
decimal.js "^10.3.1"
|
||||||
|
escape-latex "^1.2.0"
|
||||||
|
fraction.js "^4.1.2"
|
||||||
|
javascript-natural-sort "^0.7.1"
|
||||||
|
seedrandom "^3.0.5"
|
||||||
|
tiny-emitter "^2.1.0"
|
||||||
|
typed-function "^2.0.0"
|
||||||
|
|
||||||
mathml-tag-names@^2.1.0:
|
mathml-tag-names@^2.1.0:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc"
|
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc"
|
||||||
@@ -18211,6 +18256,11 @@ seedrandom@2.4.3:
|
|||||||
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.3.tgz#2438504dad33917314bff18ac4d794f16d6aaecc"
|
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-2.4.3.tgz#2438504dad33917314bff18ac4d794f16d6aaecc"
|
||||||
integrity sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=
|
integrity sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw=
|
||||||
|
|
||||||
|
seedrandom@^3.0.5:
|
||||||
|
version "3.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7"
|
||||||
|
integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==
|
||||||
|
|
||||||
select-hose@^2.0.0:
|
select-hose@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
|
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
|
||||||
@@ -19769,6 +19819,11 @@ timsort@^0.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||||
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
||||||
|
|
||||||
|
tiny-emitter@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
|
||||||
|
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
|
||||||
|
|
||||||
tiny-invariant@^1.0.2:
|
tiny-invariant@^1.0.2:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73"
|
||||||
@@ -20049,6 +20104,11 @@ type@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
|
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
|
||||||
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
|
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
|
||||||
|
|
||||||
|
typed-function@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-2.0.0.tgz#15ab3825845138a8b1113bd89e60cd6a435739e8"
|
||||||
|
integrity sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA==
|
||||||
|
|
||||||
typedarray@^0.0.6:
|
typedarray@^0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
|
||||||
|
|||||||
Reference in new issue
Block a user