Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a62c28b3f | ||
|
|
98a649b455 | ||
|
|
3529474a16 | ||
|
|
0c37a406d9 | ||
|
|
e8d56c11f0 | ||
|
|
a6b92a4505 | ||
|
|
a4c9a86772 | ||
|
|
73e440968c | ||
|
|
3f7eb8a5d3 | ||
|
|
7b2d44f2c1 |
No files matched your search
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.3.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.2...@ohif/extension-cornerstone@2.3.0) (2020-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.2.2](https://github.com/OHIF/Viewers/compare/@ohif/extension-cornerstone@2.2.1...@ohif/extension-cornerstone@2.2.2) (2020-01-28)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-cornerstone
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("../../babel.config.js");
|
||||
@@ -0,0 +1,13 @@
|
||||
const base = require('../../jest.config.base.js');
|
||||
const pkg = require('./package');
|
||||
|
||||
module.exports = {
|
||||
...base,
|
||||
name: pkg.name,
|
||||
displayName: pkg.name,
|
||||
// rootDir: "../.."
|
||||
// testMatch: [
|
||||
// //`<rootDir>/platform/${pack.name}/**/*.spec.js`
|
||||
// "<rootDir>/platform/viewer/**/*.test.js"
|
||||
// ]
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-cornerstone",
|
||||
"version": "2.2.2",
|
||||
"version": "2.3.0",
|
||||
"description": "OHIF extension for Cornerstone",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -25,7 +25,9 @@
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "yarn run dev"
|
||||
"start": "yarn run dev",
|
||||
"test:unit": "jest --watchAll",
|
||||
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
|
||||
@@ -5,6 +5,7 @@ import csTools from 'cornerstone-tools';
|
||||
import merge from 'lodash.merge';
|
||||
import queryString from 'query-string';
|
||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||
|
||||
function fallbackMetaDataProvider(type, imageId) {
|
||||
if (!imageId.includes('wado?requestType=WADO')) {
|
||||
@@ -17,7 +18,7 @@ function fallbackMetaDataProvider(type, imageId) {
|
||||
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot;
|
||||
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${
|
||||
qs.seriesUID
|
||||
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
|
||||
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
|
||||
|
||||
return cornerstone.metaData.get(type, wadoRsImageId);
|
||||
}
|
||||
@@ -32,9 +33,9 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
||||
* @param {Object|Array} configuration.csToolsConfig
|
||||
*/
|
||||
export default function init({ servicesManager, configuration }) {
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
const { UIDialogService } = servicesManager.services;
|
||||
const { UIDialogService, MeasurementService } = servicesManager.services;
|
||||
|
||||
const callInputDialog = (data, event, callback) => {
|
||||
if (UIDialogService) {
|
||||
let dialogId = UIDialogService.create({
|
||||
centralize: true,
|
||||
@@ -107,6 +108,9 @@ export default function init({ servicesManager, configuration }) {
|
||||
tools.push(...toolsGroupedByType[toolsGroup])
|
||||
);
|
||||
|
||||
/* Measurement Service */
|
||||
_connectToolsToMeasurementService(MeasurementService);
|
||||
|
||||
/* Add extension tools configuration here. */
|
||||
const internalToolsConfig = {
|
||||
ArrowAnnotate: {
|
||||
@@ -182,3 +186,111 @@ export default function init({ servicesManager, configuration }) {
|
||||
csTools.setToolActive('ZoomTouchPinch', {});
|
||||
csTools.setToolEnabled('Overlay', {});
|
||||
}
|
||||
|
||||
const _initMeasurementService = measurementService => {
|
||||
/* Initialization */
|
||||
const { toAnnotation, toMeasurement } = measurementServiceMappingsFactory(measurementService);
|
||||
const csToolsVer4MeasurementSource = measurementService.createSource(
|
||||
'CornerstoneTools',
|
||||
'4'
|
||||
);
|
||||
|
||||
/* Matching Criterias */
|
||||
const matchingCriteria = {
|
||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||
points: 2,
|
||||
};
|
||||
|
||||
/* Mappings */
|
||||
measurementService.addMapping(
|
||||
csToolsVer4MeasurementSource,
|
||||
'Length',
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
return csToolsVer4MeasurementSource;
|
||||
};
|
||||
|
||||
const _connectToolsToMeasurementService = measurementService => {
|
||||
const csToolsVer4MeasurementSource = _initMeasurementService(measurementService);
|
||||
const {
|
||||
id: sourceId,
|
||||
addOrUpdate,
|
||||
getAnnotation,
|
||||
} = csToolsVer4MeasurementSource;
|
||||
|
||||
/* Measurement Service Events */
|
||||
cornerstone.events.addEventListener(
|
||||
cornerstone.EVENTS.ELEMENT_ENABLED,
|
||||
event => {
|
||||
const {
|
||||
MEASUREMENT_ADDED,
|
||||
MEASUREMENT_UPDATED,
|
||||
} = measurementService.EVENTS;
|
||||
|
||||
measurementService.subscribe(
|
||||
MEASUREMENT_ADDED,
|
||||
({ source, measurement }) => {
|
||||
if (![sourceId].includes(source.id)) {
|
||||
const annotation = getAnnotation('Length', measurement.id);
|
||||
|
||||
console.log(
|
||||
'Measurement Service [Cornerstone]: Measurement added',
|
||||
measurement
|
||||
);
|
||||
console.log('Mapped annotation:', annotation);
|
||||
}
|
||||
});
|
||||
|
||||
measurementService.subscribe(
|
||||
MEASUREMENT_UPDATED,
|
||||
({ source, measurement }) => {
|
||||
if (![sourceId].includes(source.id)) {
|
||||
const annotation = getAnnotation('Length', measurement.id);
|
||||
|
||||
console.log(
|
||||
'Measurement Service [Cornerstone]: Measurement updated',
|
||||
measurement
|
||||
);
|
||||
console.log('Mapped annotation:', annotation);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const addOrUpdateMeasurement = csToolsAnnotation => {
|
||||
try {
|
||||
const { toolName, toolType, measurementData } = csToolsAnnotation;
|
||||
const csTool = toolName || measurementData.toolType || toolType;
|
||||
csToolsAnnotation.id = measurementData._measurementServiceId;
|
||||
const measurementServiceId = addOrUpdate(csTool, csToolsAnnotation);
|
||||
|
||||
if (!measurementData._measurementServiceId) {
|
||||
addMeasurementServiceId(measurementServiceId, csToolsAnnotation);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to add or update measurement:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const addMeasurementServiceId = (id, csToolsAnnotation) => {
|
||||
const { measurementData } = csToolsAnnotation;
|
||||
Object.assign(measurementData, { _measurementServiceId: id });
|
||||
};
|
||||
|
||||
[
|
||||
csTools.EVENTS.MEASUREMENT_ADDED,
|
||||
csTools.EVENTS.MEASUREMENT_MODIFIED,
|
||||
].forEach(csToolsEvtName => {
|
||||
event.detail.element.addEventListener(
|
||||
csToolsEvtName,
|
||||
({ detail: csToolsAnnotation }) => {
|
||||
console.log(`Cornerstone Element Event: ${csToolsEvtName}`);
|
||||
addOrUpdateMeasurement(csToolsAnnotation);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
|
||||
const SUPPORTED_TOOLS = ['Length', 'EllipticalRoi', 'RectangleRoi', 'ArrowAnnotate'];
|
||||
|
||||
const measurementServiceMappingsFactory = measurementService => {
|
||||
/**
|
||||
* Maps measurement service format object to cornerstone annotation object.
|
||||
*
|
||||
* @param {Measurement} measurement The measurement instance
|
||||
* @param {string} definition The source definition
|
||||
* @return {Object} Cornerstone annotation data
|
||||
*/
|
||||
const toAnnotation = (measurement, definition) => {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
description,
|
||||
points,
|
||||
unit,
|
||||
sopInstanceUID,
|
||||
frameOfReferenceUID,
|
||||
referenceSeriesUID,
|
||||
} = measurement;
|
||||
|
||||
return {
|
||||
toolName: definition,
|
||||
measurementData: {
|
||||
sopInstanceUid: sopInstanceUID,
|
||||
frameOfReferenceUid: frameOfReferenceUID,
|
||||
seriesInstanceUid: referenceSeriesUID,
|
||||
unit,
|
||||
text: label,
|
||||
description,
|
||||
handles: _getHandlesFromPoints(points),
|
||||
_measurementServiceId: id,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
const toMeasurement = csToolsAnnotation => {
|
||||
const { element, measurementData } = csToolsAnnotation;
|
||||
const tool =
|
||||
csToolsAnnotation.toolType ||
|
||||
csToolsAnnotation.toolName ||
|
||||
measurementData.toolType;
|
||||
|
||||
const validToolType = toolName => SUPPORTED_TOOLS.includes(toolName);
|
||||
|
||||
if (!validToolType(tool)) {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const {
|
||||
sopInstanceUid,
|
||||
frameOfReferenceUid,
|
||||
seriesInstanceUid,
|
||||
} = _getAttributes(element);
|
||||
|
||||
const points = [];
|
||||
points.push(measurementData.handles);
|
||||
|
||||
return {
|
||||
id: measurementData._measurementServiceId,
|
||||
sopInstanceUID: sopInstanceUid,
|
||||
frameOfReferenceUID: frameOfReferenceUid,
|
||||
referenceSeriesUID: seriesInstanceUid,
|
||||
label: measurementData.text,
|
||||
description: measurementData.description,
|
||||
unit: measurementData.unit,
|
||||
area: measurementData.cachedStats && measurementData.cachedStats.area, /* TODO: Add concept names instead (descriptor) */
|
||||
type: _getValueTypeFromToolType(tool),
|
||||
points: _getPointsFromHandles(measurementData.handles),
|
||||
};
|
||||
};
|
||||
|
||||
const _getAttributes = element => {
|
||||
const enabledElement = cornerstone.getEnabledElement(element);
|
||||
const imageId = enabledElement.image.imageId;
|
||||
const sopInstance = cornerstone.metaData.get('instance', imageId);
|
||||
const sopInstanceUid = sopInstance.sopInstanceUid;
|
||||
const frameOfReferenceUid = sopInstance.frameOfReferenceUID;
|
||||
const series = cornerstone.metaData.get('series', imageId);
|
||||
const seriesInstanceUid = series.seriesInstanceUid;
|
||||
|
||||
return { sopInstanceUid, frameOfReferenceUid, seriesInstanceUid };
|
||||
};
|
||||
|
||||
const _getValueTypeFromToolType = toolType => {
|
||||
const { POLYLINE, ELLIPSE, POINT } = measurementService.VALUE_TYPES;
|
||||
|
||||
/* TODO: Relocate static value types */
|
||||
const TOOL_TYPE_TO_VALUE_TYPE = {
|
||||
Length: POLYLINE,
|
||||
EllipticalRoi: ELLIPSE,
|
||||
RectangleRoi: POLYLINE,
|
||||
ArrowAnnotate: POINT,
|
||||
};
|
||||
|
||||
return TOOL_TYPE_TO_VALUE_TYPE[toolType];
|
||||
};
|
||||
|
||||
const _getPointsFromHandles = handles => {
|
||||
let points = [];
|
||||
Object.keys(handles).map(handle => {
|
||||
if (['start', 'end'].includes(handle)) {
|
||||
let point = {};
|
||||
if (handles[handle].x) point.x = handles[handle].x;
|
||||
if (handles[handle].y) point.y = handles[handle].y;
|
||||
points.push(point);
|
||||
}
|
||||
});
|
||||
return points;
|
||||
};
|
||||
|
||||
const _getHandlesFromPoints = points => {
|
||||
return points
|
||||
.map((p, i) => (i % 10 === 0 ? { start: p } : { end: p }))
|
||||
.reduce((obj, item) => Object.assign(obj, { ...item }), {});
|
||||
};
|
||||
|
||||
return {
|
||||
toAnnotation,
|
||||
toMeasurement,
|
||||
};
|
||||
};
|
||||
|
||||
export default measurementServiceMappingsFactory;
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import measurementServiceMappingsFactory from './measurementServiceMappingsFactory';
|
||||
|
||||
jest.mock('cornerstone-core', () => ({
|
||||
...jest.requireActual('cornerstone-core'),
|
||||
getEnabledElement: () => ({
|
||||
image: { imageId: 123 },
|
||||
}),
|
||||
metaData: {
|
||||
...jest.requireActual('cornerstone-core').metaData,
|
||||
get: () => ({
|
||||
sopInstanceUid: "123",
|
||||
frameOfReferenceUID: "123",
|
||||
seriesInstanceUid: "123",
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('measurementServiceMappings.js', () => {
|
||||
let mappings;
|
||||
let handles;
|
||||
let points;
|
||||
let csToolsAnnotation;
|
||||
let measurement;
|
||||
let measurementServiceMock;
|
||||
let definition = 'Length';
|
||||
|
||||
beforeEach(() => {
|
||||
measurementServiceMock = {
|
||||
VALUE_TYPES: {
|
||||
POLYLINE: 'value_type::polyline',
|
||||
POINT: 'value_type::point',
|
||||
ELLIPSE: 'value_type::ellipse',
|
||||
MULTIPOINT: 'value_type::multipoint',
|
||||
CIRCLE: 'value_type::circle',
|
||||
},
|
||||
};
|
||||
mappings = measurementServiceMappingsFactory(measurementServiceMock);
|
||||
handles = { start: { x: 1, y: 2 }, end: { x: 1, y: 2 } };
|
||||
points = [{ x: 1, y: 2 }, { x: 1, y: 2 }];
|
||||
csToolsAnnotation = {
|
||||
toolName: definition,
|
||||
measurementData: {
|
||||
_measurementServiceId: 1,
|
||||
sopInstanceUid: '123',
|
||||
frameOfReferenceUid: '123',
|
||||
seriesInstanceUid: '123',
|
||||
handles,
|
||||
text: 'Test',
|
||||
description: 'Test',
|
||||
unit: 'mm',
|
||||
},
|
||||
};
|
||||
measurement = {
|
||||
id: 1,
|
||||
sopInstanceUID: '123',
|
||||
frameOfReferenceUID: '123',
|
||||
referenceSeriesUID: '123',
|
||||
label: 'Test',
|
||||
description: 'Test',
|
||||
unit: 'mm',
|
||||
type: measurementServiceMock.VALUE_TYPES.POLYLINE,
|
||||
points: points,
|
||||
};
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('toAnnotation()', () => {
|
||||
it('map measurement service format to annotation', async () => {
|
||||
const mappedMeasurement = await mappings.toAnnotation(measurement, definition);
|
||||
expect(mappedMeasurement).toEqual(csToolsAnnotation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toMeasurement()', () => {
|
||||
it('map annotation to measurement service format', async () => {
|
||||
const mappedAnnotation = await mappings.toMeasurement(csToolsAnnotation);
|
||||
expect(mappedAnnotation).toEqual(measurement);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
const merge = require('webpack-merge');
|
||||
const path = require('path');
|
||||
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
|
||||
const pkg = require('./../package.json');
|
||||
|
||||
const ROOT_DIR = path.join(__dirname, './..');
|
||||
const SRC_DIR = path.join(__dirname, '../src');
|
||||
const DIST_DIR = path.join(__dirname, '../dist');
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
|
||||
|
||||
return merge(commonConfig, {
|
||||
devtool: 'source-map',
|
||||
stats: {
|
||||
colors: true,
|
||||
hash: true,
|
||||
timings: true,
|
||||
assets: true,
|
||||
chunks: false,
|
||||
chunkModules: false,
|
||||
modules: false,
|
||||
children: false,
|
||||
warnings: true,
|
||||
},
|
||||
optimization: {
|
||||
minimize: true,
|
||||
sideEffects: true,
|
||||
},
|
||||
output: {
|
||||
path: ROOT_DIR,
|
||||
library: 'OHIFExtLesionTracker',
|
||||
libraryTarget: 'umd',
|
||||
libraryExport: 'default',
|
||||
filename: pkg.main,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.2.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-lesion-tracker@0.1.0...@ohif/extension-lesion-tracker@0.2.0) (2020-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Lesion tracker right panel ([#1428](https://github.com/OHIF/Viewers/issues/1428)) ([98a649b](https://github.com/OHIF/Viewers/commit/98a649b455ffc712938fc5035cdef40695e58440))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# 0.1.0 (2020-02-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* lesion-tracker extension ([#1420](https://github.com/OHIF/Viewers/issues/1420)) ([73e4409](https://github.com/OHIF/Viewers/commit/73e440968ce4699d081a9c9f2d21dd68095b3056))
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Open Health Imaging Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,38 @@
|
||||
# @ohif/extension-lesion-tracker
|
||||
|
||||
This project is an OHIF extension that can be used with the Core OHIF Platform,
|
||||
either at runtime, or as an ES6 dependency, to create a medical image viewing
|
||||
application similar to that of the [legacy Lesion
|
||||
Tracker][legacy-lesion-tracker] viewer.
|
||||
|
||||
## About
|
||||
|
||||
...
|
||||
|
||||
## Scope
|
||||
|
||||
...
|
||||
|
||||
### Configuration
|
||||
|
||||
...
|
||||
|
||||
### Extensions
|
||||
|
||||
...
|
||||
|
||||
## Build & Deploy
|
||||
|
||||
...
|
||||
|
||||
## Funding/Support
|
||||
|
||||
...
|
||||
|
||||
<!--
|
||||
LINKS
|
||||
-->
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
[legacy-lesion-tracker]: http://lesiontracker.ohif.org/studylist
|
||||
<!-- prettier-ignore-end -->
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@ohif/extension-lesion-tracker",
|
||||
"version": "0.2.0",
|
||||
"description": "OHIF extension for Lesion Tracker",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
"repository": "OHIF/Viewers",
|
||||
"main": "dist/index.umd.js",
|
||||
"module": "src/index.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1.16.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
|
||||
"dev:lesion-tracker": "yarn run dev",
|
||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||
"build:package": "yarn run build",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"start": "yarn run dev"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ohif/core": "^0.50.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"classnames": "^2.2.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
.MeasurementComparisonTable {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .displayTexts {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .displayTexts .measurementDisplayText {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader {
|
||||
display: flex;
|
||||
position: relative;
|
||||
padding-left: 38px;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .warning-status {
|
||||
left: 0;
|
||||
top: 3px;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .warning-status .warning-border {
|
||||
padding: 2px 3px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .warning-status svg {
|
||||
width: 20px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .measurementTableHeader .measurementTableHeaderItem {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .generate-report {
|
||||
background-color: #151a1f;
|
||||
margin-top: 2px;
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.MeasurementComparisonTable .generate-report button {
|
||||
font-size: 12px;
|
||||
color: black;
|
||||
background-color: var(--active-color);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
|
||||
import { MeasurementTable } from '@ohif/ui';
|
||||
|
||||
import './MeasurementComparisonTable.css';
|
||||
|
||||
const overallWarnings = {
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
};
|
||||
|
||||
const measurements = [
|
||||
{
|
||||
measurementId: '125',
|
||||
measurementNumber: '125',
|
||||
itemNumber: 1,
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '12.5 x 4.6' }],
|
||||
},
|
||||
{
|
||||
measurementId: '124',
|
||||
measurementNumber: '124',
|
||||
itemNumber: 2,
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '32.5 x 1.6' }],
|
||||
},
|
||||
{
|
||||
measurementId: '123',
|
||||
measurementNumber: '123',
|
||||
itemNumber: 3,
|
||||
hasWarnings: true,
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '5.5 x 9.2' }],
|
||||
},
|
||||
];
|
||||
|
||||
const additionalFindings = [
|
||||
{
|
||||
measurementId: '122',
|
||||
measurementNumber: '122',
|
||||
itemNumber: 1,
|
||||
hasWarnings: true,
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '23.5 x 9.2' }],
|
||||
},
|
||||
{
|
||||
measurementId: '121',
|
||||
measurementNumber: '121',
|
||||
itemNumber: 2,
|
||||
hasWarnings: true,
|
||||
warningList: [
|
||||
'All measurements should have a location',
|
||||
'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR',
|
||||
],
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '11.2 x 9.2' }],
|
||||
},
|
||||
{
|
||||
measurementId: '120',
|
||||
measurementNumber: '120',
|
||||
itemNumber: 3,
|
||||
label: '(No description)',
|
||||
data: [{ displayText: '2.9 x 9.2' }],
|
||||
},
|
||||
];
|
||||
|
||||
const currentCollections = [
|
||||
{
|
||||
selectorAction: () => { },
|
||||
maxMeasurements: 3,
|
||||
groupName: 'Measurements',
|
||||
measurements: measurements,
|
||||
},
|
||||
{
|
||||
selectorAction: () => { },
|
||||
groupName: 'Additional Findings',
|
||||
measurements: additionalFindings,
|
||||
},
|
||||
];
|
||||
|
||||
const comparisonColletions = [
|
||||
{
|
||||
selectorAction: () => { },
|
||||
maxMeasurements: 3,
|
||||
groupName: 'Measurements',
|
||||
measurements: measurements,
|
||||
},
|
||||
{
|
||||
selectorAction: () => { },
|
||||
groupName: 'Additional Findings',
|
||||
measurements: additionalFindings,
|
||||
},
|
||||
];
|
||||
|
||||
const comparisonCollections = currentCollections.map((group, index) => {
|
||||
return {
|
||||
...group,
|
||||
measurements: group.measurements.map((measurement, measurementIndex) => {
|
||||
const comparisonCollection = comparisonColletions[index].measurements;
|
||||
if (measurementIndex < comparisonCollection.length) {
|
||||
return {
|
||||
...measurement,
|
||||
data: [
|
||||
...measurement.data,
|
||||
...comparisonCollection[measurementIndex].data,
|
||||
],
|
||||
};
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const comparisonTimepoints = [
|
||||
{
|
||||
key: 'Current',
|
||||
date: '10-Apr-18',
|
||||
},
|
||||
{
|
||||
key: 'Comparison',
|
||||
date: '15-Jun-18',
|
||||
}
|
||||
];
|
||||
|
||||
const MeasurementComparisonTable = () => {
|
||||
return (
|
||||
<div className="MeasurementComparisonTable">
|
||||
<MeasurementTable
|
||||
timepoints={comparisonTimepoints}
|
||||
overallWarnings={overallWarnings}
|
||||
measurementCollection={comparisonCollections}
|
||||
onRelabelClick={() => { }}
|
||||
onEditDescriptionClick={() => { }}
|
||||
/>
|
||||
<div className="generate-report">
|
||||
<button className="btn btn-primary">Generate Report</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MeasurementComparisonTable;
|
||||
@@ -0,0 +1,52 @@
|
||||
import MeasurementComparisonTable from './components/MeasurementComparisonTable';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* Only required property. Should be a unique value across all extensions.
|
||||
*/
|
||||
id: 'lesion-tracker',
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
*/
|
||||
getPanelModule({ servicesManager, commandsManager }) {
|
||||
return {
|
||||
menuOptions: [
|
||||
{
|
||||
icon: 'th-list',
|
||||
label: 'Measurements',
|
||||
target: 'lesion-tracker-panel',
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
id: 'lesion-tracker-panel',
|
||||
component: MeasurementComparisonTable,
|
||||
},
|
||||
],
|
||||
defaultContext: ['VIEWER']
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
* @returns Object
|
||||
*/
|
||||
getToolbarModule() {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} params
|
||||
* @param {ServicesManager} params.servicesManager
|
||||
* @param {CommandsManager} params.commandsManager
|
||||
* @returns Object
|
||||
*/
|
||||
getCommandsModule() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
@@ -3,6 +3,38 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.2.17](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.16...@ohif/extension-vtk@1.2.17) (2020-02-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.16](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.15...@ohif/extension-vtk@1.2.16) (2020-02-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.15](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.14...@ohif/extension-vtk@1.2.15) (2020-02-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.14](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.13...@ohif/extension-vtk@1.2.14) (2020-02-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/extension-vtk
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.2.13](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.12...@ohif/extension-vtk@1.2.13) (2020-01-30)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/extension-vtk",
|
||||
"version": "1.2.13",
|
||||
"version": "1.2.17",
|
||||
"description": "OHIF extension for VTK.js",
|
||||
"author": "OHIF",
|
||||
"license": "MIT",
|
||||
@@ -52,8 +52,8 @@
|
||||
"react-vtkjs-viewport": "^0.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ohif/core": "^2.3.7",
|
||||
"@ohif/ui": "^1.1.9",
|
||||
"@ohif/core": "^2.4.0",
|
||||
"@ohif/ui": "^1.2.0",
|
||||
"cornerstone-tools": "^4.12.0",
|
||||
"cornerstone-wado-image-loader": "^3.0.0",
|
||||
"dicom-parser": "^1.8.3",
|
||||
|
||||
+2
-1
@@ -10,7 +10,8 @@ module.exports = {
|
||||
// https://jestjs.io/docs/en/configuration#projects-array-string-projectconfig
|
||||
projects: [
|
||||
// "<rootDir>/extensions/*/jest.config.js",
|
||||
"<rootDir>/platform/*/jest.config.js"
|
||||
"<rootDir>/platform/*/jest.config.js",
|
||||
"<rootDir>/extensions/*/jest.config.js"
|
||||
],
|
||||
coverageDirectory: "<rootDir>/coverage/"
|
||||
};
|
||||
@@ -13,11 +13,13 @@
|
||||
"scripts": {
|
||||
"cm": "npx git-cz",
|
||||
"build": "lerna run build:viewer --stream",
|
||||
"build:lt": "lerna run build:viewer:lesion-tracker --stream",
|
||||
"build:ci": "lerna run build:viewer:ci --stream",
|
||||
"build:demo": "lerna run build:viewer:demo --stream",
|
||||
"build:package": "lerna run build:viewer:package --stream",
|
||||
"build:package-all": "lerna run build:package --parallel --stream",
|
||||
"dev": "lerna run dev:viewer --stream",
|
||||
"dev:lt": "lerna run dev:viewer:lesion-tracker --stream",
|
||||
"dev:project": ".scripts/dev.sh",
|
||||
"dev:orthanc": "lerna run dev:orthanc --stream",
|
||||
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [2.4.0](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.9...@ohif/core@2.4.0) (2020-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.9](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.8...@ohif/core@2.3.9) (2020-02-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/core
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.8](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.7...@ohif/core@2.3.8) (2020-02-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Remove trash data from redux storage after updates ([#1358](https://github.com/OHIF/Viewers/issues/1358)) ([7b2d44f](https://github.com/OHIF/Viewers/commit/7b2d44f2c18241ea521b8d3652aee32e36eaddb8))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [2.3.7](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.6...@ohif/core@2.3.7) (2020-01-30)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/core",
|
||||
"version": "2.3.7",
|
||||
"version": "2.4.0",
|
||||
"description": "Generic business logic for web-based medical imaging applications",
|
||||
"author": "OHIF Core Team",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
} from './services';
|
||||
|
||||
const OHIF = {
|
||||
@@ -55,6 +56,7 @@ const OHIF = {
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -85,6 +87,7 @@ export {
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('Top level exports', () => {
|
||||
'UINotificationService',
|
||||
'UIModalService',
|
||||
'UIDialogService',
|
||||
'MeasurementService',
|
||||
//
|
||||
'utils',
|
||||
'studies',
|
||||
|
||||
@@ -11,20 +11,63 @@ import {
|
||||
SET_VIEWPORT_LAYOUT_AND_DATA,
|
||||
} from './../constants/ActionTypes.js';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
export const DEFAULT_STATE = {
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
activeViewportIndex: 0,
|
||||
layout: {
|
||||
viewports: [
|
||||
{
|
||||
// plugin: 'cornerstone',
|
||||
},
|
||||
],
|
||||
viewports: [{}],
|
||||
},
|
||||
viewportSpecificData: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Take the new number of rows and columns, delete all not used viewport data and also set
|
||||
* active viewport as default in case current one is not available anymore.
|
||||
*
|
||||
* @param {Number} numRows
|
||||
* @param {Number} numColumns
|
||||
* @param {Object} currentViewportSpecificData
|
||||
* @returns
|
||||
*/
|
||||
const findActiveViewportSpecificData = (
|
||||
numRows,
|
||||
numColumns,
|
||||
currentViewportSpecificData = {}
|
||||
) => {
|
||||
const numberOfViewports = numRows * numColumns;
|
||||
const viewportSpecificData = cloneDeep(currentViewportSpecificData);
|
||||
|
||||
if (numberOfViewports < Object.keys(viewportSpecificData).length) {
|
||||
Object.keys(viewportSpecificData).forEach(key => {
|
||||
if (key > numberOfViewports - 1) {
|
||||
delete viewportSpecificData[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return viewportSpecificData;
|
||||
};
|
||||
/**
|
||||
* Take new number of rows and columns and make sure the current active viewport index is still available, if not, return the default
|
||||
*
|
||||
* @param {Number} numRows
|
||||
* @param {Number} numColumns
|
||||
* @param {Number} currentActiveViewportIndex
|
||||
* @returns
|
||||
*/
|
||||
const getActiveViewportIndex = (
|
||||
numRows,
|
||||
numColumns,
|
||||
currentActiveViewportIndex
|
||||
) => {
|
||||
const numberOfViewports = numRows * numColumns;
|
||||
|
||||
return currentActiveViewportIndex > numberOfViewports - 1
|
||||
? DEFAULT_STATE.activeViewportIndex
|
||||
: currentActiveViewportIndex;
|
||||
};
|
||||
|
||||
/**
|
||||
* The definition of a viewport action.
|
||||
*
|
||||
@@ -50,7 +93,12 @@ const viewports = (state = DEFAULT_STATE, action) => {
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_VIEWPORT_ACTIVE: {
|
||||
return { ...state, activeViewportIndex: action.viewportIndex };
|
||||
const activeViewportIndex = getActiveViewportIndex(
|
||||
state.numRows,
|
||||
state.numColumns,
|
||||
action.viewportIndex
|
||||
);
|
||||
return { ...state, activeViewportIndex };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,11 +107,25 @@ const viewports = (state = DEFAULT_STATE, action) => {
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_VIEWPORT_LAYOUT: {
|
||||
const { numRows, numColumns } = action;
|
||||
const viewportSpecificData = findActiveViewportSpecificData(
|
||||
numRows,
|
||||
numColumns,
|
||||
state.viewportSpecificData
|
||||
);
|
||||
const activeViewportIndex = getActiveViewportIndex(
|
||||
numRows,
|
||||
numColumns,
|
||||
state.activeViewportIndex
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
numRows: action.numRows,
|
||||
numColumns: action.numColumns,
|
||||
layout: { viewports: [...action.viewports] },
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,12 +135,25 @@ const viewports = (state = DEFAULT_STATE, action) => {
|
||||
* @return {Object} New state.
|
||||
*/
|
||||
case SET_VIEWPORT_LAYOUT_AND_DATA: {
|
||||
const { numRows, numColumns } = action;
|
||||
const viewportSpecificData = findActiveViewportSpecificData(
|
||||
numRows,
|
||||
numColumns,
|
||||
action.viewportSpecificData
|
||||
);
|
||||
const activeViewportIndex = getActiveViewportIndex(
|
||||
numRows,
|
||||
numColumns,
|
||||
state.activeViewportIndex
|
||||
);
|
||||
|
||||
return {
|
||||
...state,
|
||||
numRows: action.numRows,
|
||||
numColumns: action.numColumns,
|
||||
layout: { viewports: [...action.viewports] },
|
||||
viewportSpecificData: cloneDeep(action.viewportSpecificData),
|
||||
viewportSpecificData,
|
||||
activeViewportIndex,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,61 @@
|
||||
// https://medium.com/@netxm/testing-redux-reducers-with-jest-6653abbfe3e1
|
||||
import reducer from './viewports.js';
|
||||
import { Reducer } from 'redux-testkit';
|
||||
|
||||
import reducer, { DEFAULT_STATE } from './viewports.js';
|
||||
import * as types from './../constants/ActionTypes.js';
|
||||
|
||||
describe('viewports reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {})).toEqual({
|
||||
activeViewportIndex: 0,
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
layout: {
|
||||
viewports: [{}],
|
||||
},
|
||||
viewportSpecificData: {},
|
||||
});
|
||||
expect(reducer(undefined, {})).toEqual(DEFAULT_STATE);
|
||||
});
|
||||
|
||||
it('should handle SET_VIEWPORT_ACTIVE', () => {
|
||||
const setViewportActiveAction = {
|
||||
it('should handle SET_VIEWPORT_ACTIVE with inexistent viewport index', () => {
|
||||
const initialState = {
|
||||
numRows: 4,
|
||||
numColumns: 4,
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: types.SET_VIEWPORT_ACTIVE,
|
||||
viewportIndex: 100,
|
||||
};
|
||||
|
||||
const updatedState = reducer({}, setViewportActiveAction);
|
||||
const expectedToChangeInState = {
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
|
||||
expect(updatedState.activeViewportIndex).toEqual(
|
||||
setViewportActiveAction.viewportIndex
|
||||
);
|
||||
Reducer(reducer)
|
||||
.withState(initialState)
|
||||
.expect(action)
|
||||
.toChangeInState(expectedToChangeInState);
|
||||
});
|
||||
|
||||
it('should handle SET_VIEWPORT_ACTIVE with existent viewport index', () => {
|
||||
const initialState = {
|
||||
numRows: 4,
|
||||
numColumns: 4,
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: types.SET_VIEWPORT_ACTIVE,
|
||||
viewportIndex: 5,
|
||||
};
|
||||
|
||||
const expectedToChangeInState = {
|
||||
activeViewportIndex: 5,
|
||||
};
|
||||
|
||||
Reducer(reducer)
|
||||
.withState(initialState)
|
||||
.expect(action)
|
||||
.toChangeInState(expectedToChangeInState);
|
||||
});
|
||||
|
||||
it('should handle SET_VIEWPORT_LAYOUT', () => {
|
||||
const setViewportLayoutAction = {
|
||||
const initialState = DEFAULT_STATE;
|
||||
|
||||
const action = {
|
||||
type: types.SET_VIEWPORT_LAYOUT,
|
||||
numRows: 1,
|
||||
numColumns: 2,
|
||||
@@ -43,18 +69,193 @@ describe('viewports reducer', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const updatedState = reducer({}, setViewportLayoutAction);
|
||||
const expectedToChangeInState = {
|
||||
numRows: 1,
|
||||
numColumns: 2,
|
||||
layout: {
|
||||
viewports: [
|
||||
{
|
||||
plugin: 'cornerstone',
|
||||
},
|
||||
{
|
||||
plugin: 'vtk',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(updatedState.numRows).toEqual(setViewportLayoutAction.numRows);
|
||||
expect(updatedState.numColumns).toEqual(setViewportLayoutAction.numColumns);
|
||||
expect(updatedState.layout.viewports).toEqual(
|
||||
setViewportLayoutAction.viewports
|
||||
);
|
||||
Reducer(reducer)
|
||||
.withState(initialState)
|
||||
.expect(action)
|
||||
.toChangeInState(expectedToChangeInState);
|
||||
});
|
||||
|
||||
it('should handle SET_VIEWPORT_LAYOUT when we reduce the number of viewports', () => {
|
||||
const initialState = {
|
||||
numRows: 1,
|
||||
numColumns: 2,
|
||||
viewportSpecificData: {
|
||||
0: { viewportData0: 'viewportData0' },
|
||||
1: { viewportData1: 'viewportData1' },
|
||||
},
|
||||
layout: {
|
||||
viewports: [],
|
||||
},
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: types.SET_VIEWPORT_LAYOUT,
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
viewports: [],
|
||||
};
|
||||
|
||||
const expectedState = {
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
viewportSpecificData: {
|
||||
0: { viewportData0: 'viewportData0' },
|
||||
},
|
||||
layout: {
|
||||
viewports: [],
|
||||
},
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
|
||||
Reducer(reducer)
|
||||
.withState(initialState)
|
||||
.expect(action)
|
||||
.toReturnState(expectedState);
|
||||
});
|
||||
|
||||
it('should handle SET_VIEWPORT_LAYOUT_AND_DATA', () => {
|
||||
const initialState = {
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
viewportSpecificData: {
|
||||
0: { viewportData0: 'data0' },
|
||||
},
|
||||
layout: {
|
||||
viewports: [{ plugin: 'cornerstone' }],
|
||||
},
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: types.SET_VIEWPORT_LAYOUT_AND_DATA,
|
||||
numRows: 1,
|
||||
numColumns: 2,
|
||||
viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }],
|
||||
viewportSpecificData: {
|
||||
0: { viewportData0: 'NEWdata0' },
|
||||
1: { viewportData1: 'NEWdata1' },
|
||||
},
|
||||
};
|
||||
|
||||
const expectedState = {
|
||||
numRows: 1,
|
||||
numColumns: 2,
|
||||
viewportSpecificData: {
|
||||
0: { viewportData0: 'NEWdata0' },
|
||||
1: { viewportData1: 'NEWdata1' },
|
||||
},
|
||||
layout: {
|
||||
viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }],
|
||||
},
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
|
||||
Reducer(reducer)
|
||||
.withState(initialState)
|
||||
.expect(action)
|
||||
.toReturnState(expectedState);
|
||||
});
|
||||
|
||||
it('should handle SET_VIEWPORT_LAYOUT_AND_DATA when we reduce the number of viewports', () => {
|
||||
const initialState = {
|
||||
numRows: 1,
|
||||
numColumns: 3,
|
||||
viewportSpecificData: {
|
||||
0: { viewportData0: 'vtkData0' },
|
||||
1: { viewportData1: 'vtkData1' },
|
||||
2: { viewportData2: 'vtkData2' },
|
||||
},
|
||||
layout: {
|
||||
viewports: [{ plugin: 'vtk' }, { plugin: 'vtk' }, { plugin: 'vtk' }],
|
||||
},
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: types.SET_VIEWPORT_LAYOUT_AND_DATA,
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
viewports: [{ plugin: 'cornerstone' }],
|
||||
viewportSpecificData: {
|
||||
0: { viewportData0: 'cornerstoneData0' },
|
||||
},
|
||||
};
|
||||
|
||||
const expectedState = {
|
||||
numRows: 1,
|
||||
numColumns: 1,
|
||||
viewportSpecificData: {
|
||||
0: { viewportData0: 'cornerstoneData0' },
|
||||
},
|
||||
layout: {
|
||||
viewports: [{ plugin: 'cornerstone' }],
|
||||
},
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
|
||||
Reducer(reducer)
|
||||
.withState(initialState)
|
||||
.expect(action)
|
||||
.toReturnState(expectedState);
|
||||
});
|
||||
|
||||
it('should handle SET_VIEWPORT when we only set one viewport specific data', () => {
|
||||
const initialState = {
|
||||
numRows: 1,
|
||||
numColumns: 2,
|
||||
viewportSpecificData: {
|
||||
0: { viewportData0: 'data0' },
|
||||
1: { viewportData1: 'data1' },
|
||||
},
|
||||
layout: {
|
||||
viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }],
|
||||
},
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: types.SET_VIEWPORT,
|
||||
viewportIndex: 1,
|
||||
viewportSpecificData: {
|
||||
viewportData1: 'NEWdata1',
|
||||
},
|
||||
};
|
||||
|
||||
const expectedState = {
|
||||
numRows: 1,
|
||||
numColumns: 2,
|
||||
viewportSpecificData: {
|
||||
0: { viewportData0: 'data0' },
|
||||
1: { viewportData1: 'NEWdata1' },
|
||||
},
|
||||
layout: {
|
||||
viewports: [{ plugin: 'cornerstone' }, { plugin: 'cornerstone' }],
|
||||
},
|
||||
activeViewportIndex: 0,
|
||||
};
|
||||
|
||||
Reducer(reducer)
|
||||
.withState(initialState)
|
||||
.expect(action)
|
||||
.toReturnState(expectedState);
|
||||
});
|
||||
|
||||
// If there were previous keys, this would have
|
||||
// "merge" behavior, not a clear & set
|
||||
// May be worth another test?
|
||||
it('should handle SET_VIEWPORT', () => {
|
||||
const viewportToSet = 0;
|
||||
const setViewportAction = {
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
import log from '../../log';
|
||||
import guid from '../../utils/guid';
|
||||
|
||||
/**
|
||||
* Measurement source schema
|
||||
*
|
||||
* @typedef {Object} MeasurementSource
|
||||
* @property {number} id -
|
||||
* @property {string} name -
|
||||
* @property {string} version -
|
||||
*/
|
||||
|
||||
/**
|
||||
* Measurement schema
|
||||
*
|
||||
* @typedef {Object} Measurement
|
||||
* @property {number} id -
|
||||
* @property {string} sopInstanceUID -
|
||||
* @property {string} frameOfReferenceUID -
|
||||
* @property {string} referenceSeriesUID -
|
||||
* @property {string} label -
|
||||
* @property {string} description -
|
||||
* @property {string} type -
|
||||
* @property {string} unit -
|
||||
* @property {number} area -
|
||||
* @property {Array} points -
|
||||
* @property {MeasurementSource} source -
|
||||
*/
|
||||
|
||||
/* Measurement schema keys for object validation. */
|
||||
const MEASUREMENT_SCHEMA_KEYS = [
|
||||
'id',
|
||||
'sopInstanceUID',
|
||||
'frameOfReferenceUID',
|
||||
'referenceSeriesUID',
|
||||
'label',
|
||||
'description',
|
||||
'type',
|
||||
'unit',
|
||||
'area', // TODO: Add concept names instead (descriptor)
|
||||
'points',
|
||||
'source',
|
||||
];
|
||||
|
||||
const EVENTS = {
|
||||
MEASUREMENT_UPDATED: 'event::measurement_updated',
|
||||
MEASUREMENT_ADDED: 'event::measurement_added',
|
||||
};
|
||||
|
||||
const VALUE_TYPES = {
|
||||
POLYLINE: 'value_type::polyline',
|
||||
POINT: 'value_type::point',
|
||||
ELLIPSE: 'value_type::ellipse',
|
||||
MULTIPOINT: 'value_type::multipoint',
|
||||
CIRCLE: 'value_type::circle',
|
||||
};
|
||||
|
||||
class MeasurementService {
|
||||
constructor() {
|
||||
this.sources = {};
|
||||
this.mappings = {};
|
||||
this.measurements = {};
|
||||
this.listeners = {};
|
||||
Object.defineProperty(this, 'EVENTS', {
|
||||
value: EVENTS,
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
});
|
||||
Object.defineProperty(this, 'VALUE_TYPES', {
|
||||
value: VALUE_TYPES,
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all measurements.
|
||||
*
|
||||
* @return {Measurement[]} Array of measurements
|
||||
*/
|
||||
getMeasurements() {
|
||||
const measurements = this._arrayOfObjects(this.measurements);
|
||||
return (
|
||||
measurements &&
|
||||
measurements.map(m => this.measurements[Object.keys(m)[0]])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific measurement by its id.
|
||||
*
|
||||
* @param {string} id If of the measurement
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
getMeasurement(id) {
|
||||
let measurement = null;
|
||||
const measurements = this.measurements[id];
|
||||
|
||||
if (measurements && Object.keys(measurements).length > 0) {
|
||||
measurement = this.measurements[id];
|
||||
}
|
||||
|
||||
return measurement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new source.
|
||||
*
|
||||
* @param {string} name Name of the source
|
||||
* @param {string} version Source name
|
||||
* @return {MeasurementSource} Measurement source instance
|
||||
*/
|
||||
createSource(name, version) {
|
||||
if (!name) {
|
||||
log.warn('Source name not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
log.warn('Source version not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = guid();
|
||||
const source = {
|
||||
id,
|
||||
name,
|
||||
version,
|
||||
};
|
||||
source.addOrUpdate = (definition, measurement) => {
|
||||
return this.addOrUpdate(source, definition, measurement);
|
||||
};
|
||||
source.getAnnotation = (definition, measurementId) => {
|
||||
return this.getAnnotation(source, definition, measurementId);
|
||||
};
|
||||
|
||||
log.info(`New '${name}@${version}' source added.`);
|
||||
this.sources[id] = source;
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new measurement matching criteria along with mapping functions.
|
||||
*
|
||||
* @param {MeasurementSource} source Measurement source instance
|
||||
* @param {string} definition Definition of the measurement (Annotation Type)
|
||||
* @param {MatchingCriteria} matchingCriteria The matching criteria
|
||||
* @param {Function} toSourceSchema Mapping function to source schema
|
||||
* @param {Function} toMeasurementSchema Mapping function to measurement schema
|
||||
* @return void
|
||||
*/
|
||||
addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toSourceSchema,
|
||||
toMeasurementSchema
|
||||
) {
|
||||
if (!this._isValidSource(source)) {
|
||||
log.warn('Invalid source. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matchingCriteria) {
|
||||
log.warn('Matching criteria not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!definition) {
|
||||
log.warn('Definition not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!toSourceSchema) {
|
||||
log.warn('Source mapping function not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!toMeasurementSchema) {
|
||||
log.warn('Measurement mapping function not provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
const mapping = {
|
||||
matchingCriteria,
|
||||
definition,
|
||||
toSourceSchema,
|
||||
toMeasurementSchema,
|
||||
};
|
||||
|
||||
if (Array.isArray(this.mappings[source.id])) {
|
||||
this.mappings[source.id].push(mapping);
|
||||
} else {
|
||||
this.mappings[source.id] = [mapping];
|
||||
}
|
||||
|
||||
log.info(`New measurement mapping added to source '${this._getSourceInfo(source)}'.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation for specific source.
|
||||
*
|
||||
* @param {MeasurementSource} source Measurement source instance
|
||||
* @param {string} definition The source definition
|
||||
* @param {string} measurementId The measurement service measurement id
|
||||
* @return {Object} Source measurement schema
|
||||
*/
|
||||
getAnnotation(source, definition, measurementId) {
|
||||
if (!this._isValidSource(source)) {
|
||||
log.warn('Invalid source. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!definition) {
|
||||
log.warn('No source definition provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
const mapping = this._getMappingByMeasurementSource(measurementId, definition);
|
||||
if (mapping) return mapping.toSourceSchema(measurement, definition);
|
||||
|
||||
const measurement = this.getMeasurement(measurementId);
|
||||
const matchingMapping = this._getMatchingMapping(source, definition, measurement);
|
||||
|
||||
if (matchingMapping) {
|
||||
log.info('Matching mapping found:', matchingMapping);
|
||||
const { toSourceSchema, definition } = matchingMapping;
|
||||
return toSourceSchema(measurement, definition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or update persisted measurements.
|
||||
*
|
||||
* @param {MeasurementSource} source The measurement source instance
|
||||
* @param {string} definition The source definition
|
||||
* @param {Measurement} measurement The source measurement
|
||||
* @return {string} A measurement id
|
||||
*/
|
||||
addOrUpdate(source, definition, sourceMeasurement) {
|
||||
if (!this._isValidSource(source)) {
|
||||
log.warn('Invalid source. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceInfo = this._getSourceInfo(source);
|
||||
|
||||
if (!definition) {
|
||||
log.warn('No source definition provided. Exiting early.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._sourceHasMappings(source)) {
|
||||
log.warn(`No measurement mappings found for '${sourceInfo}' source. Exiting early.`);
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
log.error(`Failed to map '${sourceInfo}' measurement for definition ${definition}:`, error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isValidMeasurement(measurement)) {
|
||||
log.warn(
|
||||
`Attempting to add or update a invalid measurement provided by '${sourceInfo}'. Exiting early.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let internalId = sourceMeasurement.id;
|
||||
if (!internalId) {
|
||||
internalId = guid();
|
||||
log.warn(`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._broadcastChange(this.EVENTS.MEASUREMENT_UPDATED, source, newMeasurement);
|
||||
} else {
|
||||
log.info(`Measurement added.`, newMeasurement);
|
||||
this.measurements[internalId] = newMeasurement;
|
||||
this._broadcastChange(this.EVENTS.MEASUREMENT_ADDED, source, newMeasurement);
|
||||
}
|
||||
|
||||
return newMeasurement.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to measurement updates.
|
||||
*
|
||||
* @param {string} eventName The name of the event
|
||||
* @param {Function} callback Events callback
|
||||
* @return {Object} Observable object with actions
|
||||
*/
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
|
||||
_getMappingByMeasurementSource(measurementId, definition) {
|
||||
const measurement = this.getMeasurement(measurementId);
|
||||
if (this._isValidSource(measurement.source)) {
|
||||
return this.mappings[measurement.source.id].find(
|
||||
m => m.definition === definition
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get measurement mapping function if matching criteria.
|
||||
*
|
||||
* @param {MeasurementSource} source Measurement source instance
|
||||
* @param {string} definition The source definition
|
||||
* @param {string} measurement The measurement serice measurement
|
||||
* @return {Object} The mapping based on matched criteria
|
||||
*/
|
||||
_getMatchingMapping(source, definition, measurement) {
|
||||
const sourceMappings = this.mappings[source.id];
|
||||
|
||||
const sourceMappingsByDefinition = sourceMappings.filter(
|
||||
mapping => mapping.definition === definition
|
||||
);
|
||||
|
||||
/* Criteria Matching */
|
||||
return sourceMappingsByDefinition.find(({ matchingCriteria }) => {
|
||||
return (
|
||||
measurement.points &&
|
||||
measurement.points.length === matchingCriteria.points
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns formatted string with source info.
|
||||
*
|
||||
* @param {MeasurementSource} source Measurement source
|
||||
* @return {string} Source information
|
||||
*/
|
||||
_getSourceInfo(source) {
|
||||
return `${source.name}@${source.version}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given source is valid.
|
||||
*
|
||||
* @param {MeasurementSource} source Measurement source
|
||||
* @return {boolean} Measurement source validation
|
||||
*/
|
||||
_isValidSource(source) {
|
||||
return source && this.sources[source.id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given source has mappings.
|
||||
*
|
||||
* @param {MeasurementSource} source The measurement source
|
||||
* @return {boolean} Validation if source has mappings
|
||||
*/
|
||||
_sourceHasMappings(source) {
|
||||
return (
|
||||
Array.isArray(this.mappings[source.id]) && this.mappings[source.id].length
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {Measurement} measurementData Measurement data
|
||||
* @return {boolean} Measurement validation
|
||||
*/
|
||||
_isValidMeasurement(measurementData) {
|
||||
Object.keys(measurementData).forEach(key => {
|
||||
if (!MEASUREMENT_SCHEMA_KEYS.includes(key)) {
|
||||
log.warn(`Invalid measurement key: ${key}`);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given measurement service event is valid.
|
||||
*
|
||||
* @param {string} eventName The name of the event
|
||||
* @return {boolean} Event name validation
|
||||
*/
|
||||
_isValidEvent(eventName) {
|
||||
return Object.values(this.EVENTS).includes(eventName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts object of objects to array.
|
||||
*
|
||||
* @return {Array} Array of objects
|
||||
*/
|
||||
_arrayOfObjects = obj => {
|
||||
return Object.entries(obj).map(e => ({ [e[0]]: e[1] }));
|
||||
};
|
||||
}
|
||||
|
||||
export default MeasurementService;
|
||||
export { EVENTS, VALUE_TYPES };
|
||||
@@ -0,0 +1,405 @@
|
||||
import MeasurementService from './MeasurementService.js';
|
||||
import log from '../../log';
|
||||
|
||||
jest.mock('../../log.js', () => ({
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('MeasurementService.js', () => {
|
||||
let measurementService;
|
||||
let measurement;
|
||||
let source;
|
||||
let definition;
|
||||
let matchingCriteria;
|
||||
let toAnnotation;
|
||||
let toMeasurement;
|
||||
let annotation;
|
||||
|
||||
beforeEach(() => {
|
||||
measurementService = new MeasurementService();
|
||||
source = measurementService.createSource('Test', '1');
|
||||
definition = 'Length';
|
||||
annotation = {
|
||||
toolName: definition,
|
||||
measurementData: {},
|
||||
};
|
||||
measurement = {
|
||||
sopInstanceUID: '123',
|
||||
frameOfReferenceUID: '1234',
|
||||
referenceSeriesUID: '12345',
|
||||
label: 'Label',
|
||||
description: 'Description',
|
||||
unit: 'mm',
|
||||
area: 123,
|
||||
type: measurementService.VALUE_TYPES.POLYLINE,
|
||||
points: [{ x: 1, y: 2 }, { x: 1, y: 2 }],
|
||||
source: source,
|
||||
};
|
||||
toAnnotation = () => annotation;
|
||||
toMeasurement = () => measurement;
|
||||
matchingCriteria = {
|
||||
valueType: measurementService.VALUE_TYPES.POLYLINE,
|
||||
points: 2,
|
||||
};
|
||||
log.warn.mockClear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('createSource()', () => {
|
||||
it('creates new source with name and version', () => {
|
||||
measurementService.createSource('Testing', '1');
|
||||
});
|
||||
|
||||
it('logs warning and return early if no name provided', () => {
|
||||
measurementService.createSource(null, '1');
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('logs warning and return early if no version provided', () => {
|
||||
measurementService.createSource('Testing', null);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMapping()', () => {
|
||||
it('adds new mapping', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
});
|
||||
|
||||
it('logs warning and return early if no matching criteria provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
null,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('logs warning and return early if invalid source provided', () => {
|
||||
const invalidSoure = {};
|
||||
|
||||
measurementService.addMapping(
|
||||
invalidSoure,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
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('logs warning and return early if no definition provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
null /* definition */,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('logs warning and return early if no measurement mapping function provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
null /* toAnnotation */,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('logs warning and return early if no annotation mapping function provided', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
null /* toMeasurement */
|
||||
);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAnnotation()', () => {
|
||||
it('get annotation based on matched criteria', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
const measurementId = source.addOrUpdate(definition, annotation);
|
||||
const mappedAnnotation = source.getAnnotation(definition, measurementId);
|
||||
|
||||
expect(annotation).toBe(mappedAnnotation);
|
||||
});
|
||||
|
||||
it('get annotation based on source and definition', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
{},
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
const measurementId = source.addOrUpdate(definition, annotation);
|
||||
const mappedAnnotation = source.getAnnotation(definition, measurementId);
|
||||
|
||||
expect(annotation).toBe(mappedAnnotation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMeasurements()', () => {
|
||||
it('return all measurement service measurements', () => {
|
||||
const anotherMeasurement = {
|
||||
...measurement,
|
||||
label: 'Label2',
|
||||
unit: 'HU',
|
||||
};
|
||||
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
source.addOrUpdate(definition, measurement);
|
||||
source.addOrUpdate(definition, anotherMeasurement);
|
||||
|
||||
const measurements = measurementService.getMeasurements();
|
||||
|
||||
expect(measurements.length).toEqual(2);
|
||||
expect(measurements.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMeasurement()', () => {
|
||||
it('return measurement service measurement with given id', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
const id = source.addOrUpdate(definition, measurement);
|
||||
const returnedMeasurement = measurementService.getMeasurement(id);
|
||||
|
||||
/* Clear dynamic data */
|
||||
delete returnedMeasurement.modifiedTimestamp;
|
||||
|
||||
expect({ id, ...measurement }).toEqual(returnedMeasurement);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addOrUpdate()', () => {
|
||||
it('adds new measurements', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
source.addOrUpdate(definition, measurement);
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
const measurements = measurementService.getMeasurements();
|
||||
|
||||
expect(measurements.length).toBe(2);
|
||||
});
|
||||
|
||||
it('fails to add new measurements when no mapping', () => {
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('fails to add new measurements when invalid mapping function', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
1 /* Invalid */
|
||||
);
|
||||
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(log.error.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('adds new measurement with custom id', () => {
|
||||
const newMeasurement = { id: 1, ...measurement };
|
||||
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
/* Add new measurement */
|
||||
source.addOrUpdate(definition, newMeasurement);
|
||||
const savedMeasurement = measurementService.getMeasurement(newMeasurement.id);
|
||||
|
||||
/* Clear dynamic data */
|
||||
delete newMeasurement.modifiedTimestamp;
|
||||
delete savedMeasurement.modifiedTimestamp;
|
||||
|
||||
expect(newMeasurement).toEqual(savedMeasurement);
|
||||
});
|
||||
|
||||
it('logs warning and return if adding invalid measurement', () => {
|
||||
measurement.invalidProperty = {};
|
||||
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(log.warn.mock.calls.length).toBe(2);
|
||||
});
|
||||
|
||||
it('updates existent measurement', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
const id = source.addOrUpdate(definition, measurement);
|
||||
|
||||
measurement.unit = 'HU';
|
||||
|
||||
source.addOrUpdate(definition, { id, ...measurement });
|
||||
const updatedMeasurement = measurementService.getMeasurement(id);
|
||||
|
||||
expect(updatedMeasurement.unit).toBe('HU');
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribe()', () => {
|
||||
it('subscribers receive broadcasted add event', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
const { MEASUREMENT_ADDED } = measurementService.EVENTS;
|
||||
let addCallbackWasCalled = false;
|
||||
|
||||
/* Subscribe to add event */
|
||||
measurementService.subscribe(
|
||||
MEASUREMENT_ADDED,
|
||||
() => (addCallbackWasCalled = true)
|
||||
);
|
||||
|
||||
/* Add new measurement */
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(addCallbackWasCalled).toBe(true);
|
||||
});
|
||||
|
||||
it('subscribers receive broadcasted update event', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
const { MEASUREMENT_UPDATED } = measurementService.EVENTS;
|
||||
let updateCallbackWasCalled = false;
|
||||
|
||||
/* Subscribe to update event */
|
||||
measurementService.subscribe(
|
||||
MEASUREMENT_UPDATED,
|
||||
() => (updateCallbackWasCalled = true)
|
||||
);
|
||||
|
||||
/* Create measurement */
|
||||
const id = source.addOrUpdate(definition, measurement);
|
||||
|
||||
/* Update measurement */
|
||||
source.addOrUpdate(definition, { id, ...measurement });
|
||||
|
||||
expect(updateCallbackWasCalled).toBe(true);
|
||||
});
|
||||
|
||||
it('unsubscribes a listener', () => {
|
||||
measurementService.addMapping(
|
||||
source,
|
||||
definition,
|
||||
matchingCriteria,
|
||||
toAnnotation,
|
||||
toMeasurement
|
||||
);
|
||||
|
||||
let updateCallbackWasCalled = false;
|
||||
const { MEASUREMENT_ADDED } = measurementService.EVENTS;
|
||||
|
||||
/* Subscribe to Add event */
|
||||
const { unsubscribe } = measurementService.subscribe(
|
||||
MEASUREMENT_ADDED,
|
||||
() => (updateCallbackWasCalled = true)
|
||||
);
|
||||
|
||||
/* Unsubscribe */
|
||||
unsubscribe();
|
||||
|
||||
/* Create measurement */
|
||||
source.addOrUpdate(definition, measurement);
|
||||
|
||||
expect(updateCallbackWasCalled).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
import MeasurementService from './MeasurementService';
|
||||
|
||||
export default {
|
||||
name: 'MeasurementService',
|
||||
create: ({ configuration = {} }) => {
|
||||
return new MeasurementService();
|
||||
},
|
||||
};
|
||||
@@ -2,10 +2,12 @@ import ServicesManager from './ServicesManager.js';
|
||||
import UINotificationService from './UINotificationService';
|
||||
import UIModalService from './UIModalService';
|
||||
import UIDialogService from './UIDialogService';
|
||||
import MeasurementService from './MeasurementService';
|
||||
|
||||
export {
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
ServicesManager,
|
||||
MeasurementService,
|
||||
};
|
||||
@@ -46,7 +46,7 @@ export default class RetrieveMetadataLoader {
|
||||
}
|
||||
|
||||
if (loaders.next().done && !result) {
|
||||
throw 'cant find data';
|
||||
throw new Error('RetrieveMetadataLoader failed');
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -67,6 +67,12 @@ function processSingleframe(instances) {
|
||||
if (instances.length > 2) {
|
||||
const firstIpp = _getImagePositionPatient(firstImage);
|
||||
const lastIpp = _getImagePositionPatient(instances[instances.length - 1]);
|
||||
|
||||
// We can't reconstruct if we are missing imagePositionPatient values
|
||||
if (!firstIpp || !lastIpp) {
|
||||
return { value: false };
|
||||
}
|
||||
|
||||
const averageSpacingBetweenFrames =
|
||||
_getPerpendicularDistance(firstIpp, lastIpp) / (instances.length - 1);
|
||||
|
||||
@@ -136,8 +142,13 @@ function _getSpacingIssue(spacing, averageSpacing) {
|
||||
}
|
||||
|
||||
function _getImagePositionPatient(instance) {
|
||||
return instance
|
||||
.getTagValue('x00200032')
|
||||
const tagValue = instance
|
||||
.getTagValue('x00200032');
|
||||
if (!tagValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
return tagValue
|
||||
.split('\\')
|
||||
.map(element => Number(element));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,17 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.2.0](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.9...@ohif/ui@1.2.0) (2020-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Lesion tracker right panel ([#1428](https://github.com/OHIF/Viewers/issues/1428)) ([98a649b](https://github.com/OHIF/Viewers/commit/98a649b455ffc712938fc5035cdef40695e58440))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [1.1.9](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.8...@ohif/ui@1.1.9) (2020-01-30)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/ui",
|
||||
"version": "1.1.9",
|
||||
"version": "1.2.0",
|
||||
"description": "A set of React components for Medical Imaging Viewers",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -184,10 +184,12 @@ class MeasurementTable extends Component {
|
||||
};
|
||||
|
||||
getTimepointsHeader = () => {
|
||||
return this.props.timepoints.map((timepoint, index) => {
|
||||
const { timepoints, t } = this.props;
|
||||
|
||||
return timepoints.map((timepoint, index) => {
|
||||
return (
|
||||
<div key={index} className="measurementTableHeaderItem">
|
||||
<div className="timepointLabel">{this.props.t(timepoint.key)}</div>
|
||||
<div className="timepointLabel">{t(timepoint.key)}</div>
|
||||
<div className="timepointDate">{timepoint.date}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
position: relative
|
||||
top: 2px
|
||||
|
||||
|
||||
.tooltip-warning
|
||||
&.in
|
||||
opacity 1
|
||||
|
||||
@@ -44,8 +44,8 @@ class MeasurementTableItem extends Component {
|
||||
<div>{this.getTableListItem()}</div>
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<React.Fragment>{this.getTableListItem()}</React.Fragment>
|
||||
)}
|
||||
<React.Fragment>{this.getTableListItem()}</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -101,7 +101,7 @@ class MeasurementTableItem extends Component {
|
||||
nsSeparator: '|',
|
||||
})}
|
||||
</div>
|
||||
<div>{this.getDataDisplayText()}</div>
|
||||
<div className="displayTexts">{this.getDataDisplayText()}</div>
|
||||
<div className="rowActions">{actionButtons}</div>
|
||||
</div>
|
||||
</TableListItem>
|
||||
|
||||
@@ -25,6 +25,20 @@ $headerRowHeight = 63px
|
||||
text-align: left
|
||||
flex: 1;
|
||||
|
||||
.tableListHeaderSelector
|
||||
cursor: pointer
|
||||
max-width: 30px
|
||||
padding-left: 2px
|
||||
align-items: stretch
|
||||
flex: 1
|
||||
justify-content: space-around
|
||||
text-align: center
|
||||
|
||||
svg
|
||||
fill: #c1d8e3
|
||||
height: 63px
|
||||
max-width: 11px
|
||||
|
||||
.numberOfItems
|
||||
color: var(--ui-sky-blue)
|
||||
float: right
|
||||
|
||||
@@ -26,6 +26,7 @@ const PUBLIC_URL = process.env.PUBLIC_URL || '/';
|
||||
const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js';
|
||||
const PROXY_TARGET = process.env.PROXY_TARGET;
|
||||
const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
|
||||
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
|
||||
|
||||
module.exports = (env, argv) => {
|
||||
const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR });
|
||||
@@ -33,6 +34,9 @@ module.exports = (env, argv) => {
|
||||
const hasProxy = PROXY_TARGET && PROXY_DOMAIN;
|
||||
|
||||
const mergedConfig = merge(baseConfig, {
|
||||
entry: {
|
||||
app: ENTRY_TARGET,
|
||||
},
|
||||
output: {
|
||||
path: DIST_DIR,
|
||||
filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
|
||||
|
||||
@@ -3,6 +3,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [3.6.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.6.0...@ohif/viewer@3.6.1) (2020-02-10)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.6.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.5.1...@ohif/viewer@3.6.0) (2020-02-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* 🎸 MeasurementService ([#1314](https://github.com/OHIF/Viewers/issues/1314)) ([0c37a40](https://github.com/OHIF/Viewers/commit/0c37a406d963569af8c3be24c697dafd42712dfc))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.5.1](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.5.0...@ohif/viewer@3.5.1) (2020-02-07)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# [3.5.0](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.14...@ohif/viewer@3.5.0) (2020-02-06)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* lesion-tracker extension ([#1420](https://github.com/OHIF/Viewers/issues/1420)) ([73e4409](https://github.com/OHIF/Viewers/commit/73e440968ce4699d081a9c9f2d21dd68095b3056))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.4.14](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.13...@ohif/viewer@3.4.14) (2020-02-06)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## [3.4.13](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.12...@ohif/viewer@3.4.13) (2020-01-30)
|
||||
|
||||
**Note:** Version bump only for package @ohif/viewer
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@ohif/viewer",
|
||||
"version": "3.4.13",
|
||||
"version": "3.6.1",
|
||||
"description": "OHIF Viewer",
|
||||
"author": "OHIF Contributors",
|
||||
"license": "MIT",
|
||||
@@ -19,12 +19,14 @@
|
||||
"scripts": {
|
||||
"build:package": "cross-env NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.commonjs.js --progress",
|
||||
"build:viewer": "cross-env NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js --progress",
|
||||
"build:viewer:lesion-tracker": "cross-env ENTRY_TARGET=index-lesion-tracker.js NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js --progress",
|
||||
"build:viewer:ci": "cross-env NODE_ENV=production PUBLIC_URL=/pwa/ APP_CONFIG=config/netlify.js QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js",
|
||||
"build:viewer:demo": "cross-env NODE_ENV=production APP_CONFIG=config/demo.js HTML_TEMPLATE=rollbar.html QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --progress --config .webpack/webpack.pwa.js",
|
||||
"build:viewer:package": "yarn run build:package",
|
||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
|
||||
"dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker_nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js --watch",
|
||||
"dev:viewer": "yarn run dev",
|
||||
"dev:viewer:lesion-tracker": "cross-env ENTRY_TARGET=index-lesion-tracker.js NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
|
||||
"start": "yarn run dev",
|
||||
"test:e2e": "cypress open",
|
||||
"test:e2e:ci": "percy exec -- cypress run --config video=false --record --browser chrome --spec 'cypress/integration/visual-regression/**/*'",
|
||||
@@ -45,14 +47,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"@ohif/core": "^2.3.7",
|
||||
"@ohif/extension-cornerstone": "^2.2.2",
|
||||
"@ohif/core": "^2.4.0",
|
||||
"@ohif/extension-cornerstone": "^2.3.0",
|
||||
"@ohif/extension-dicom-html": "^1.1.0",
|
||||
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
||||
"@ohif/extension-dicom-pdf": "^1.0.1",
|
||||
"@ohif/extension-vtk": "^1.2.13",
|
||||
"@ohif/extension-lesion-tracker": "^0.2.0",
|
||||
"@ohif/extension-vtk": "^1.2.17",
|
||||
"@ohif/i18n": "^0.52.5",
|
||||
"@ohif/ui": "^1.1.9",
|
||||
"@ohif/ui": "^1.2.0",
|
||||
"@tanem/react-nprogress": "^1.1.25",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.2.1",
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
utils,
|
||||
redux as reduxOHIF,
|
||||
} from '@ohif/core';
|
||||
@@ -122,7 +123,12 @@ class App extends Component {
|
||||
} = this._appConfig;
|
||||
|
||||
this.initUserManager(oidc);
|
||||
_initServices([UINotificationService, UIModalService, UIDialogService]);
|
||||
_initServices([
|
||||
UINotificationService,
|
||||
UIModalService,
|
||||
UIDialogService,
|
||||
MeasurementService,
|
||||
]);
|
||||
_initExtensions(
|
||||
[...defaultExtensions, ...extensions],
|
||||
cornerstoneExtensionConfig,
|
||||
@@ -144,6 +150,7 @@ class App extends Component {
|
||||
UINotificationService,
|
||||
UIDialogService,
|
||||
UIModalService,
|
||||
MeasurementService,
|
||||
} = servicesManager.services;
|
||||
|
||||
if (this._userManager) {
|
||||
|
||||
@@ -253,6 +253,7 @@ function ViewerRetrieveStudyData({
|
||||
.catch(error => {
|
||||
if (error && !error.isCanceled) {
|
||||
setError(true);
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -290,11 +291,13 @@ function ViewerRetrieveStudyData({
|
||||
.catch(error => {
|
||||
if (error && !error.isCanceled) {
|
||||
setError(true);
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
setError(true);
|
||||
log.error(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Entry point for development and production PWA builds.
|
||||
* Packaged (NPM) builds go through `index-umd.js`
|
||||
*/
|
||||
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import App from './App.js';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
/**
|
||||
* EXTENSIONS
|
||||
* =================
|
||||
*
|
||||
* Importing and modifying the extensions our app uses HERE allows us to leverage
|
||||
* tree shaking and a few other niceties. However, by including them here they become
|
||||
* "baked in" to the published application.
|
||||
*
|
||||
* Depending on your use case/needs, you may want to consider not adding any extensions
|
||||
* by default HERE, and instead provide them via the extensions configuration key or
|
||||
* by using the exported `App` component, and passing in your extensions as props using
|
||||
* the defaultExtensions property.
|
||||
*/
|
||||
import OHIFLesionTrackerExtension from '@ohif/extension-lesion-tracker';
|
||||
import OHIFDicomPDFExtension from '@ohif/extension-dicom-pdf';
|
||||
|
||||
/*
|
||||
* Default Settings
|
||||
*/
|
||||
let config = {};
|
||||
|
||||
if (window) {
|
||||
config = window.config || {};
|
||||
}
|
||||
|
||||
const appProps = {
|
||||
config,
|
||||
defaultExtensions: [OHIFLesionTrackerExtension, OHIFDicomPDFExtension],
|
||||
};
|
||||
|
||||
/** Create App */
|
||||
const app = React.createElement(App, appProps, null);
|
||||
|
||||
/** Render */
|
||||
ReactDOM.render(app, document.getElementById('root'));
|
||||
@@ -1100,34 +1100,13 @@
|
||||
pirates "^4.0.0"
|
||||
source-map-support "^0.5.9"
|
||||
|
||||
"@babel/runtime@7.1.2":
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.1.2.tgz#81c89935f4647706fc54541145e6b4ecfef4b8e3"
|
||||
integrity sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.12.0"
|
||||
|
||||
"@babel/runtime@7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.6.0.tgz#4fc1d642a9fd0299754e8b5de62c631cf5568205"
|
||||
integrity sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@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.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":
|
||||
version "7.5.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
|
||||
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3":
|
||||
version "7.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.3.tgz#0811944f73a6c926bb2ad35e918dcc1bfab279f1"
|
||||
integrity sha512-fVHx1rzEmwB130VTkLnxR+HmxcTjGzH12LYQcFFoBwakMd3aOMD4OsRN7tGG/UOYE2ektgFrS8uACAoRk1CY0w==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4", "@babel/template@^7.6.0":
|
||||
version "7.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.6.0.tgz#7f0159c7f5012230dad64cca42ec9bdb5c9536e6"
|
||||
@@ -1283,54 +1262,6 @@
|
||||
unique-filename "^1.1.1"
|
||||
which "^1.3.1"
|
||||
|
||||
"@ffmpeg-installer/darwin-x64@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz#48e1706c690e628148482bfb64acb67472089aaa"
|
||||
integrity sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==
|
||||
|
||||
"@ffmpeg-installer/ffmpeg@1.0.20":
|
||||
version "1.0.20"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.0.20.tgz#d3c9c2bbcd76149468fb0886c2b3fe9e4795490b"
|
||||
integrity sha512-wbgd//6OdwbFXYgV68ZyKrIcozEQpUKlvV66XHaqO2h3sFbX0jYLzx62Q0v8UcFWN21LoxT98NU2P+K0OWsKNA==
|
||||
optionalDependencies:
|
||||
"@ffmpeg-installer/darwin-x64" "4.1.0"
|
||||
"@ffmpeg-installer/linux-arm" "4.1.3"
|
||||
"@ffmpeg-installer/linux-arm64" "4.1.4"
|
||||
"@ffmpeg-installer/linux-ia32" "4.1.0"
|
||||
"@ffmpeg-installer/linux-x64" "4.1.0"
|
||||
"@ffmpeg-installer/win32-ia32" "4.1.0"
|
||||
"@ffmpeg-installer/win32-x64" "4.1.0"
|
||||
|
||||
"@ffmpeg-installer/linux-arm64@4.1.4":
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz#7219f3f901bb67f7926cb060b56b6974a6cad29f"
|
||||
integrity sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==
|
||||
|
||||
"@ffmpeg-installer/linux-arm@4.1.3":
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz#c554f105ed5f10475ec25d7bec94926ce18db4c1"
|
||||
integrity sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==
|
||||
|
||||
"@ffmpeg-installer/linux-ia32@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz#adad70b0d0d9d8d813983d6e683c5a338a75e442"
|
||||
integrity sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==
|
||||
|
||||
"@ffmpeg-installer/linux-x64@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz#b4a5d89c4e12e6d9306dbcdc573df716ec1c4323"
|
||||
integrity sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==
|
||||
|
||||
"@ffmpeg-installer/win32-ia32@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz#6eac4fb691b64c02e7a116c1e2d167f3e9b40638"
|
||||
integrity sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==
|
||||
|
||||
"@ffmpeg-installer/win32-x64@4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz#17e8699b5798d4c60e36e2d6326a8ebe5e95a2c5"
|
||||
integrity sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==
|
||||
|
||||
"@hapi/address@2.x.x":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.2.tgz#1c794cd6dbf2354d1eb1ef10e0303f573e1c7222"
|
||||
@@ -11845,11 +11776,6 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||
|
||||
lodash@4.17.11:
|
||||
version "4.17.11"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
|
||||
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
|
||||
|
||||
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:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
@@ -15681,10 +15607,10 @@ react-transition-group@^4.1.1:
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-vtkjs-viewport@^0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/react-vtkjs-viewport/-/react-vtkjs-viewport-0.8.2.tgz#cdccf156cb089d32b935ebc3dc5f31213ba17bef"
|
||||
integrity sha512-2OPbU5y4ay+E6qfZw4sW19uqfBnNw9kAF1PaZ/Sk5XMEEKnebplEof9ZZtE6nhPImFqMCfu4TXzYVHIAPmwFYA==
|
||||
react-vtkjs-viewport@^0.8.3:
|
||||
version "0.8.4"
|
||||
resolved "https://registry.yarnpkg.com/react-vtkjs-viewport/-/react-vtkjs-viewport-0.8.4.tgz#6bd9837d7762b845e77f053fb8fbf4469428d594"
|
||||
integrity sha512-b90ENJzmStiahxVox+7mMwZij3NVUu//jE07PV2Qtp6E7q/eLXn6ZUwcEt9w5YHN36xLZVcx9b53ztLcXVgh/Q==
|
||||
dependencies:
|
||||
date-fns "^2.2.1"
|
||||
gl-matrix "^3.1.0"
|
||||
@@ -16059,11 +15985,6 @@ regenerator-runtime@^0.11.0, regenerator-runtime@^0.11.1:
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
|
||||
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
|
||||
|
||||
regenerator-runtime@^0.12.0:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
|
||||
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
|
||||
|
||||
regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.2:
|
||||
version "0.13.3"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
|
||||
|
||||
Reference in new issue
Block a user