Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a62c28b3f | ||
|
|
98a649b455 | ||
|
|
3529474a16 | ||
|
|
0c37a406d9 | ||
|
|
e8d56c11f0 | ||
|
|
a6b92a4505 | ||
|
|
a4c9a86772 | ||
|
|
73e440968c | ||
|
|
3f7eb8a5d3 | ||
|
|
7b2d44f2c1 | ||
|
|
225da81135 | ||
|
|
686d12da5c | ||
|
|
c22756051b | ||
|
|
8fdfcc0c7d | ||
|
|
44ab65c981 | ||
|
|
b9574b6efc | ||
|
|
96a8e26786 | ||
|
|
80f5b23622 |
No files matched your search
@@ -3,6 +3,17 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [2.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)
|
## [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
|
**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",
|
"name": "@ohif/extension-cornerstone",
|
||||||
"version": "2.2.2",
|
"version": "2.3.0",
|
||||||
"description": "OHIF extension for Cornerstone",
|
"description": "OHIF extension for Cornerstone",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -25,7 +25,9 @@
|
|||||||
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
|
||||||
"build:package": "yarn run build",
|
"build:package": "yarn run build",
|
||||||
"prepublishOnly": "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": {
|
"peerDependencies": {
|
||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import csTools from 'cornerstone-tools';
|
|||||||
import merge from 'lodash.merge';
|
import merge from 'lodash.merge';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import initCornerstoneTools from './initCornerstoneTools.js';
|
import initCornerstoneTools from './initCornerstoneTools.js';
|
||||||
|
import measurementServiceMappingsFactory from './utils/measurementServiceMappings/measurementServiceMappingsFactory';
|
||||||
|
|
||||||
function fallbackMetaDataProvider(type, imageId) {
|
function fallbackMetaDataProvider(type, imageId) {
|
||||||
if (!imageId.includes('wado?requestType=WADO')) {
|
if (!imageId.includes('wado?requestType=WADO')) {
|
||||||
@@ -17,7 +18,7 @@ function fallbackMetaDataProvider(type, imageId) {
|
|||||||
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot;
|
const wadoRoot = window.store.getState().servers.servers[0].wadoRoot;
|
||||||
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${
|
const wadoRsImageId = `wadors:${wadoRoot}/studies/${qs.studyUID}/series/${
|
||||||
qs.seriesUID
|
qs.seriesUID
|
||||||
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
|
}/instances/${qs.objectUID}/frames/${qs.frame || 1}`;
|
||||||
|
|
||||||
return cornerstone.metaData.get(type, wadoRsImageId);
|
return cornerstone.metaData.get(type, wadoRsImageId);
|
||||||
}
|
}
|
||||||
@@ -32,9 +33,9 @@ cornerstone.metaData.addProvider(fallbackMetaDataProvider, -1);
|
|||||||
* @param {Object|Array} configuration.csToolsConfig
|
* @param {Object|Array} configuration.csToolsConfig
|
||||||
*/
|
*/
|
||||||
export default function init({ servicesManager, configuration }) {
|
export default function init({ servicesManager, configuration }) {
|
||||||
const callInputDialog = (data, event, callback) => {
|
const { UIDialogService, MeasurementService } = servicesManager.services;
|
||||||
const { UIDialogService } = servicesManager.services;
|
|
||||||
|
|
||||||
|
const callInputDialog = (data, event, callback) => {
|
||||||
if (UIDialogService) {
|
if (UIDialogService) {
|
||||||
let dialogId = UIDialogService.create({
|
let dialogId = UIDialogService.create({
|
||||||
centralize: true,
|
centralize: true,
|
||||||
@@ -107,6 +108,9 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
tools.push(...toolsGroupedByType[toolsGroup])
|
tools.push(...toolsGroupedByType[toolsGroup])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* Measurement Service */
|
||||||
|
_connectToolsToMeasurementService(MeasurementService);
|
||||||
|
|
||||||
/* Add extension tools configuration here. */
|
/* Add extension tools configuration here. */
|
||||||
const internalToolsConfig = {
|
const internalToolsConfig = {
|
||||||
ArrowAnnotate: {
|
ArrowAnnotate: {
|
||||||
@@ -182,3 +186,111 @@ export default function init({ servicesManager, configuration }) {
|
|||||||
csTools.setToolActive('ZoomTouchPinch', {});
|
csTools.setToolActive('ZoomTouchPinch', {});
|
||||||
csTools.setToolEnabled('Overlay', {});
|
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,57 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Set VTK viewport as active by interaction ([#1139](https://github.com/OHIF/Viewers/issues/1139)) ([686d12d](https://github.com/OHIF/Viewers/commit/686d12da5c9d3d435b1e326c2a5caee36e2ed27c))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [1.2.12](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.11...@ohif/extension-vtk@1.2.12) (2020-01-30)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.2.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.10...@ohif/extension-vtk@1.2.11) (2020-01-28)
|
## [1.2.11](https://github.com/OHIF/Viewers/compare/@ohif/extension-vtk@1.2.10...@ohif/extension-vtk@1.2.11) (2020-01-28)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/extension-vtk
|
**Note:** Version bump only for package @ohif/extension-vtk
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/extension-vtk",
|
"name": "@ohif/extension-vtk",
|
||||||
"version": "1.2.11",
|
"version": "1.2.17",
|
||||||
"description": "OHIF extension for VTK.js",
|
"description": "OHIF extension for VTK.js",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -49,11 +49,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"react-vtkjs-viewport": "^0.3.9"
|
"react-vtkjs-viewport": "^0.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ohif/core": "^2.3.6",
|
"@ohif/core": "^2.4.0",
|
||||||
"@ohif/ui": "^1.1.8",
|
"@ohif/ui": "^1.2.0",
|
||||||
"cornerstone-tools": "^4.12.0",
|
"cornerstone-tools": "^4.12.0",
|
||||||
"cornerstone-wado-image-loader": "^3.0.0",
|
"cornerstone-wado-image-loader": "^3.0.0",
|
||||||
"dicom-parser": "^1.8.3",
|
"dicom-parser": "^1.8.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import { View2D } from 'react-vtkjs-viewport';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import VTKViewport from './VTKViewport';
|
||||||
|
|
||||||
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
const { setViewportActive, setViewportSpecificData } = OHIF.redux.actions;
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ const mapStateToProps = (state, ownProps) => {
|
|||||||
const pluginDetails = viewportLayout.vtk || {};
|
const pluginDetails = viewportLayout.vtk || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
activeViewportIndex: state.viewports.activeViewportIndex,
|
||||||
layout: state.viewports.layout,
|
layout: state.viewports.layout,
|
||||||
isActive,
|
isActive,
|
||||||
...pluginDetails,
|
...pluginDetails,
|
||||||
@@ -75,6 +76,6 @@ const ConnectedVTKViewport = connect(
|
|||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps,
|
mapDispatchToProps,
|
||||||
mergeProps
|
mergeProps
|
||||||
)(View2D);
|
)(VTKViewport);
|
||||||
|
|
||||||
export default ConnectedVTKViewport;
|
export default ConnectedVTKViewport;
|
||||||
@@ -77,6 +77,11 @@ class OHIFVTKViewport extends Component {
|
|||||||
}),
|
}),
|
||||||
viewportIndex: PropTypes.number,
|
viewportIndex: PropTypes.number,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
onScroll: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onScroll: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
static id = 'OHIFVTKViewport';
|
static id = 'OHIFVTKViewport';
|
||||||
@@ -386,6 +391,7 @@ class OHIFVTKViewport extends Component {
|
|||||||
this.state.paintFilterBackgroundImageData
|
this.state.paintFilterBackgroundImageData
|
||||||
}
|
}
|
||||||
viewportIndex={this.props.viewportIndex}
|
viewportIndex={this.props.viewportIndex}
|
||||||
|
onScroll={this.props.onScroll}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.vtk-viewport-handler svg {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React, { useEffect, useCallback } from 'react';
|
||||||
|
import { View2D } from 'react-vtkjs-viewport';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import './VTKViewport.css';
|
||||||
|
|
||||||
|
const VTKViewport = props => {
|
||||||
|
const style = { width: '100%', height: '100%', position: 'relative' };
|
||||||
|
|
||||||
|
const setViewportActiveHandler = useCallback(() => {
|
||||||
|
const { setViewportActive, viewportIndex, activeViewportIndex } = props;
|
||||||
|
|
||||||
|
if (viewportIndex !== activeViewportIndex) {
|
||||||
|
// set in Connected
|
||||||
|
setViewportActive();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScrollEvent = evt => {
|
||||||
|
const vtkViewportApiReference = props.onScroll(props.viewportIndex) || {};
|
||||||
|
const viewportUid = vtkViewportApiReference.uid;
|
||||||
|
const viewportWasScrolled = viewportUid === evt.detail.uid;
|
||||||
|
|
||||||
|
if (viewportWasScrolled) {
|
||||||
|
setViewportActiveHandler();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('vtkscrollevent', handleScrollEvent);
|
||||||
|
return () =>
|
||||||
|
window.removeEventListener('vtkscrollevent', handleScrollEvent);
|
||||||
|
}, [props, props.onScroll, props.viewportIndex, setViewportActiveHandler]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="vtk-viewport-handler"
|
||||||
|
style={style}
|
||||||
|
onClick={setViewportActiveHandler}
|
||||||
|
>
|
||||||
|
<View2D {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
VTKViewport.propTypes = {
|
||||||
|
setViewportActive: PropTypes.func.isRequired,
|
||||||
|
viewportIndex: PropTypes.number.isRequired,
|
||||||
|
activeViewportIndex: PropTypes.number.isRequired,
|
||||||
|
/* Receives viewportIndex */
|
||||||
|
onScroll: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
VTKViewport.defaultProps = {
|
||||||
|
onScroll: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VTKViewport;
|
||||||
@@ -103,6 +103,9 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
|
getVtkApis: ({ index }) => {
|
||||||
|
return apis[index];
|
||||||
|
},
|
||||||
axial: async ({ viewports }) => {
|
axial: async ({ viewports }) => {
|
||||||
const api = await _getActiveViewportVTKApi(viewports);
|
const api = await _getActiveViewportVTKApi(viewports);
|
||||||
|
|
||||||
@@ -266,11 +269,12 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
'crosshairsWidget'
|
'crosshairsWidget'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const uid = api.uid;
|
||||||
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
const istyle = vtkInteractorStyleMPRCrosshairs.newInstance();
|
||||||
|
|
||||||
api.setInteractorStyle({
|
api.setInteractorStyle({
|
||||||
istyle,
|
istyle,
|
||||||
configuration: { apis, apiIndex },
|
configuration: { apis, apiIndex, uid },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -296,55 +300,45 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
},
|
},
|
||||||
enableRotateTool: {
|
enableRotateTool: {
|
||||||
commandFn: actions.enableRotateTool,
|
commandFn: actions.enableRotateTool,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
enableCrosshairsTool: {
|
enableCrosshairsTool: {
|
||||||
commandFn: actions.enableCrosshairsTool,
|
commandFn: actions.enableCrosshairsTool,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
enableLevelTool: {
|
enableLevelTool: {
|
||||||
commandFn: actions.enableLevelTool,
|
commandFn: actions.enableLevelTool,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
setBlendModeToComposite: {
|
setBlendModeToComposite: {
|
||||||
commandFn: actions.setBlendModeToComposite,
|
commandFn: actions.setBlendModeToComposite,
|
||||||
storeContexts: [],
|
|
||||||
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
options: { blendMode: BlendMode.COMPOSITE_BLEND },
|
||||||
},
|
},
|
||||||
setBlendModeToMaximumIntensity: {
|
setBlendModeToMaximumIntensity: {
|
||||||
commandFn: actions.setBlendModeToMaximumIntensity,
|
commandFn: actions.setBlendModeToMaximumIntensity,
|
||||||
storeContexts: [],
|
|
||||||
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
options: { blendMode: BlendMode.MAXIMUM_INTENSITY_BLEND },
|
||||||
},
|
},
|
||||||
setBlendModeToMinimumIntensity: {
|
setBlendModeToMinimumIntensity: {
|
||||||
commandFn: actions.setBlendMode,
|
commandFn: actions.setBlendMode,
|
||||||
storeContexts: [],
|
|
||||||
options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND },
|
options: { blendMode: BlendMode.MINIMUM_INTENSITY_BLEND },
|
||||||
},
|
},
|
||||||
setBlendModeToAverageIntensity: {
|
setBlendModeToAverageIntensity: {
|
||||||
commandFn: actions.setBlendMode,
|
commandFn: actions.setBlendMode,
|
||||||
storeContexts: [],
|
|
||||||
options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND },
|
options: { blendMode: BlendMode.AVERAGE_INTENSITY_BLEND },
|
||||||
},
|
},
|
||||||
setSlabThickness: {
|
setSlabThickness: {
|
||||||
// TODO: How do we pass in a function argument?
|
// TODO: How do we pass in a function argument?
|
||||||
commandFn: actions.setSlabThickness,
|
commandFn: actions.setSlabThickness,
|
||||||
storeContexts: [],
|
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
increaseSlabThickness: {
|
increaseSlabThickness: {
|
||||||
commandFn: actions.changeSlabThickness,
|
commandFn: actions.changeSlabThickness,
|
||||||
storeContexts: [],
|
|
||||||
options: {
|
options: {
|
||||||
change: 3,
|
change: 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
decreaseSlabThickness: {
|
decreaseSlabThickness: {
|
||||||
commandFn: actions.changeSlabThickness,
|
commandFn: actions.changeSlabThickness,
|
||||||
storeContexts: [],
|
|
||||||
options: {
|
options: {
|
||||||
change: -3,
|
change: -3,
|
||||||
},
|
},
|
||||||
@@ -355,6 +349,10 @@ const commandsModule = ({ commandsManager }) => {
|
|||||||
options: {},
|
options: {},
|
||||||
context: 'VIEWER',
|
context: 'VIEWER',
|
||||||
},
|
},
|
||||||
|
getVtkApiForViewportIndex: {
|
||||||
|
commandFn: actions.getVtkApis,
|
||||||
|
context: 'VIEWER',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncComponent from './asyncComponent.js';
|
import asyncComponent from './asyncComponent.js';
|
||||||
import commandsModule from './commandsModule.js';
|
import commandsModule from './commandsModule.js';
|
||||||
import toolbarModule from './toolbarModule.js';
|
import toolbarModule from './toolbarModule.js';
|
||||||
|
import withCommandsManager from './withCommandsManager.js';
|
||||||
// This feels weird
|
// This feels weird
|
||||||
// import loadLocales from './loadLocales';
|
// import loadLocales from './loadLocales';
|
||||||
|
|
||||||
@@ -14,8 +15,8 @@ const vtkExtension = {
|
|||||||
*/
|
*/
|
||||||
id: 'vtk',
|
id: 'vtk',
|
||||||
|
|
||||||
getViewportModule() {
|
getViewportModule({ commandsManager }) {
|
||||||
return OHIFVTKViewport;
|
return withCommandsManager(OHIFVTKViewport, commandsManager);
|
||||||
},
|
},
|
||||||
getToolbarModule() {
|
getToolbarModule() {
|
||||||
return toolbarModule;
|
return toolbarModule;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default function withCommandsManager(Component, commandsManager = {}) {
|
||||||
|
return class WithCommandsManager extends React.Component {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
{...this.props}
|
||||||
|
onScroll={viewportIndex =>
|
||||||
|
commandsManager.runCommand('getVtkApiForViewportIndex', {
|
||||||
|
index: viewportIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
+2
-1
@@ -10,7 +10,8 @@ module.exports = {
|
|||||||
// https://jestjs.io/docs/en/configuration#projects-array-string-projectconfig
|
// https://jestjs.io/docs/en/configuration#projects-array-string-projectconfig
|
||||||
projects: [
|
projects: [
|
||||||
// "<rootDir>/extensions/*/jest.config.js",
|
// "<rootDir>/extensions/*/jest.config.js",
|
||||||
"<rootDir>/platform/*/jest.config.js"
|
"<rootDir>/platform/*/jest.config.js",
|
||||||
|
"<rootDir>/extensions/*/jest.config.js"
|
||||||
],
|
],
|
||||||
coverageDirectory: "<rootDir>/coverage/"
|
coverageDirectory: "<rootDir>/coverage/"
|
||||||
};
|
};
|
||||||
+3
-1
@@ -13,11 +13,13 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"cm": "npx git-cz",
|
"cm": "npx git-cz",
|
||||||
"build": "lerna run build:viewer --stream",
|
"build": "lerna run build:viewer --stream",
|
||||||
|
"build:lt": "lerna run build:viewer:lesion-tracker --stream",
|
||||||
"build:ci": "lerna run build:viewer:ci --stream",
|
"build:ci": "lerna run build:viewer:ci --stream",
|
||||||
"build:demo": "lerna run build:viewer:demo --stream",
|
"build:demo": "lerna run build:viewer:demo --stream",
|
||||||
"build:package": "lerna run build:viewer:package --stream",
|
"build:package": "lerna run build:viewer:package --stream",
|
||||||
"build:package-all": "lerna run build:package --parallel --stream",
|
"build:package-all": "lerna run build:package --parallel --stream",
|
||||||
"dev": "lerna run dev:viewer --stream",
|
"dev": "lerna run dev:viewer --stream",
|
||||||
|
"dev:lt": "lerna run dev:viewer:lesion-tracker --stream",
|
||||||
"dev:project": ".scripts/dev.sh",
|
"dev:project": ".scripts/dev.sh",
|
||||||
"dev:orthanc": "lerna run dev:orthanc --stream",
|
"dev:orthanc": "lerna run dev:orthanc --stream",
|
||||||
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
|
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",
|
||||||
@@ -59,6 +61,7 @@
|
|||||||
"copy-webpack-plugin": "^5.0.3",
|
"copy-webpack-plugin": "^5.0.3",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"css-loader": "^3.2.0",
|
"css-loader": "^3.2.0",
|
||||||
|
"cypress-file-upload": "^3.5.3",
|
||||||
"dotenv": "^8.1.0",
|
"dotenv": "^8.1.0",
|
||||||
"eslint": "5.16.0",
|
"eslint": "5.16.0",
|
||||||
"eslint-config-prettier": "^6.4.0",
|
"eslint-config-prettier": "^6.4.0",
|
||||||
@@ -72,7 +75,6 @@
|
|||||||
"eslint-plugin-react": "7.x",
|
"eslint-plugin-react": "7.x",
|
||||||
"eslint-plugin-react-hooks": "1.5.0",
|
"eslint-plugin-react-hooks": "1.5.0",
|
||||||
"extract-css-chunks-webpack-plugin": "^4.5.4",
|
"extract-css-chunks-webpack-plugin": "^4.5.4",
|
||||||
"favicons-webpack-plugin": "^1.0.2",
|
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"husky": "^3.0.0",
|
"husky": "^3.0.0",
|
||||||
"jest": "^24.8.0",
|
"jest": "^24.8.0",
|
||||||
|
|||||||
@@ -3,6 +3,47 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [2.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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Set VTK viewport as active by interaction ([#1139](https://github.com/OHIF/Viewers/issues/1139)) ([686d12d](https://github.com/OHIF/Viewers/commit/686d12da5c9d3d435b1e326c2a5caee36e2ed27c))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [2.3.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.5...@ohif/core@2.3.6) (2020-01-28)
|
## [2.3.6](https://github.com/OHIF/Viewers/compare/@ohif/core@2.3.5...@ohif/core@2.3.6) (2020-01-28)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/core
|
**Note:** Version bump only for package @ohif/core
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/core",
|
"name": "@ohif/core",
|
||||||
"version": "2.3.6",
|
"version": "2.4.0",
|
||||||
"description": "Generic business logic for web-based medical imaging applications",
|
"description": "Generic business logic for web-based medical imaging applications",
|
||||||
"author": "OHIF Core Team",
|
"author": "OHIF Core Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export class CommandsManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { commandFn, storeContexts } = definition;
|
const { commandFn, storeContexts = [] } = definition;
|
||||||
const definitionOptions = definition.options;
|
const definitionOptions = definition.options;
|
||||||
|
|
||||||
let commandParams = {};
|
let commandParams = {};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
|
MeasurementService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
const OHIF = {
|
const OHIF = {
|
||||||
@@ -55,6 +56,7 @@ const OHIF = {
|
|||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
|
MeasurementService,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -85,6 +87,7 @@ export {
|
|||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
|
MeasurementService,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { OHIF };
|
export { OHIF };
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ describe('Top level exports', () => {
|
|||||||
'UINotificationService',
|
'UINotificationService',
|
||||||
'UIModalService',
|
'UIModalService',
|
||||||
'UIDialogService',
|
'UIDialogService',
|
||||||
|
'MeasurementService',
|
||||||
//
|
//
|
||||||
'utils',
|
'utils',
|
||||||
'studies',
|
'studies',
|
||||||
|
|||||||
@@ -11,20 +11,63 @@ import {
|
|||||||
SET_VIEWPORT_LAYOUT_AND_DATA,
|
SET_VIEWPORT_LAYOUT_AND_DATA,
|
||||||
} from './../constants/ActionTypes.js';
|
} from './../constants/ActionTypes.js';
|
||||||
|
|
||||||
const DEFAULT_STATE = {
|
export const DEFAULT_STATE = {
|
||||||
numRows: 1,
|
numRows: 1,
|
||||||
numColumns: 1,
|
numColumns: 1,
|
||||||
activeViewportIndex: 0,
|
activeViewportIndex: 0,
|
||||||
layout: {
|
layout: {
|
||||||
viewports: [
|
viewports: [{}],
|
||||||
{
|
|
||||||
// plugin: 'cornerstone',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
viewportSpecificData: {},
|
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.
|
* The definition of a viewport action.
|
||||||
*
|
*
|
||||||
@@ -50,7 +93,12 @@ const viewports = (state = DEFAULT_STATE, action) => {
|
|||||||
* @return {Object} New state.
|
* @return {Object} New state.
|
||||||
*/
|
*/
|
||||||
case SET_VIEWPORT_ACTIVE: {
|
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.
|
* @return {Object} New state.
|
||||||
*/
|
*/
|
||||||
case SET_VIEWPORT_LAYOUT: {
|
case SET_VIEWPORT_LAYOUT: {
|
||||||
|
const { numRows, numColumns } = action;
|
||||||
|
const viewportSpecificData = findActiveViewportSpecificData(
|
||||||
|
numRows,
|
||||||
|
numColumns,
|
||||||
|
state.viewportSpecificData
|
||||||
|
);
|
||||||
|
const activeViewportIndex = getActiveViewportIndex(
|
||||||
|
numRows,
|
||||||
|
numColumns,
|
||||||
|
state.activeViewportIndex
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
numRows: action.numRows,
|
numRows: action.numRows,
|
||||||
numColumns: action.numColumns,
|
numColumns: action.numColumns,
|
||||||
layout: { viewports: [...action.viewports] },
|
layout: { viewports: [...action.viewports] },
|
||||||
|
viewportSpecificData,
|
||||||
|
activeViewportIndex,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,12 +135,25 @@ const viewports = (state = DEFAULT_STATE, action) => {
|
|||||||
* @return {Object} New state.
|
* @return {Object} New state.
|
||||||
*/
|
*/
|
||||||
case SET_VIEWPORT_LAYOUT_AND_DATA: {
|
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 {
|
return {
|
||||||
...state,
|
...state,
|
||||||
numRows: action.numRows,
|
numRows: action.numRows,
|
||||||
numColumns: action.numColumns,
|
numColumns: action.numColumns,
|
||||||
layout: { viewports: [...action.viewports] },
|
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 'redux-testkit';
|
||||||
import reducer from './viewports.js';
|
|
||||||
|
import reducer, { DEFAULT_STATE } from './viewports.js';
|
||||||
import * as types from './../constants/ActionTypes.js';
|
import * as types from './../constants/ActionTypes.js';
|
||||||
|
|
||||||
describe('viewports reducer', () => {
|
describe('viewports reducer', () => {
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
expect(reducer(undefined, {})).toEqual({
|
expect(reducer(undefined, {})).toEqual(DEFAULT_STATE);
|
||||||
activeViewportIndex: 0,
|
|
||||||
numRows: 1,
|
|
||||||
numColumns: 1,
|
|
||||||
layout: {
|
|
||||||
viewports: [{}],
|
|
||||||
},
|
|
||||||
viewportSpecificData: {},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle SET_VIEWPORT_ACTIVE', () => {
|
it('should handle SET_VIEWPORT_ACTIVE with inexistent viewport index', () => {
|
||||||
const setViewportActiveAction = {
|
const initialState = {
|
||||||
|
numRows: 4,
|
||||||
|
numColumns: 4,
|
||||||
|
activeViewportIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = {
|
||||||
type: types.SET_VIEWPORT_ACTIVE,
|
type: types.SET_VIEWPORT_ACTIVE,
|
||||||
viewportIndex: 100,
|
viewportIndex: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedState = reducer({}, setViewportActiveAction);
|
const expectedToChangeInState = {
|
||||||
|
activeViewportIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
expect(updatedState.activeViewportIndex).toEqual(
|
Reducer(reducer)
|
||||||
setViewportActiveAction.viewportIndex
|
.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', () => {
|
it('should handle SET_VIEWPORT_LAYOUT', () => {
|
||||||
const setViewportLayoutAction = {
|
const initialState = DEFAULT_STATE;
|
||||||
|
|
||||||
|
const action = {
|
||||||
type: types.SET_VIEWPORT_LAYOUT,
|
type: types.SET_VIEWPORT_LAYOUT,
|
||||||
numRows: 1,
|
numRows: 1,
|
||||||
numColumns: 2,
|
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);
|
Reducer(reducer)
|
||||||
expect(updatedState.numColumns).toEqual(setViewportLayoutAction.numColumns);
|
.withState(initialState)
|
||||||
expect(updatedState.layout.viewports).toEqual(
|
.expect(action)
|
||||||
setViewportLayoutAction.viewports
|
.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', () => {
|
it('should handle SET_VIEWPORT', () => {
|
||||||
const viewportToSet = 0;
|
const viewportToSet = 0;
|
||||||
const setViewportAction = {
|
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 UINotificationService from './UINotificationService';
|
||||||
import UIModalService from './UIModalService';
|
import UIModalService from './UIModalService';
|
||||||
import UIDialogService from './UIDialogService';
|
import UIDialogService from './UIDialogService';
|
||||||
|
import MeasurementService from './MeasurementService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
ServicesManager,
|
ServicesManager,
|
||||||
|
MeasurementService,
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,7 @@ export default class RetrieveMetadataLoader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (loaders.next().done && !result) {
|
if (loaders.next().done && !result) {
|
||||||
throw 'cant find data';
|
throw new Error('RetrieveMetadataLoader failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ function processSingleframe(instances) {
|
|||||||
if (instances.length > 2) {
|
if (instances.length > 2) {
|
||||||
const firstIpp = _getImagePositionPatient(firstImage);
|
const firstIpp = _getImagePositionPatient(firstImage);
|
||||||
const lastIpp = _getImagePositionPatient(instances[instances.length - 1]);
|
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 =
|
const averageSpacingBetweenFrames =
|
||||||
_getPerpendicularDistance(firstIpp, lastIpp) / (instances.length - 1);
|
_getPerpendicularDistance(firstIpp, lastIpp) / (instances.length - 1);
|
||||||
|
|
||||||
@@ -136,8 +142,13 @@ function _getSpacingIssue(spacing, averageSpacing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _getImagePositionPatient(instance) {
|
function _getImagePositionPatient(instance) {
|
||||||
return instance
|
const tagValue = instance
|
||||||
.getTagValue('x00200032')
|
.getTagValue('x00200032');
|
||||||
|
if (!tagValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagValue
|
||||||
.split('\\')
|
.split('\\')
|
||||||
.map(element => Number(element));
|
.map(element => Number(element));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,17 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [0.52.5](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.4...@ohif/i18n@0.52.5) (2020-01-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* download tool fixes & improvements ([#1235](https://github.com/OHIF/Viewers/issues/1235)) ([b9574b6](https://github.com/OHIF/Viewers/commit/b9574b6efcfeb85cde35b5cae63282f8e1b35be6))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [0.52.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.3...@ohif/i18n@0.52.4) (2019-12-16)
|
## [0.52.4](https://github.com/OHIF/Viewers/compare/@ohif/i18n@0.52.3...@ohif/i18n@0.52.4) (2019-12-16)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/i18n
|
**Note:** Version bump only for package @ohif/i18n
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/i18n",
|
"name": "@ohif/i18n",
|
||||||
"version": "0.52.4",
|
"version": "0.52.5",
|
||||||
"description": "Internationalization library for The OHIF Viewer",
|
"description": "Internationalization library for The OHIF Viewer",
|
||||||
"author": "OHIF",
|
"author": "OHIF",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"emptyFilenameError": "The file name cannot be empty.",
|
||||||
|
"fileType": "File Type",
|
||||||
|
"filename": "File Name",
|
||||||
|
"formTitle": "Please specify the dimensions, filename, and desired type for the output image.",
|
||||||
|
"imageHeight": "Image height (px)",
|
||||||
|
"imagePreview": "Image Preview",
|
||||||
|
"imageWidth": "Image width (px)",
|
||||||
|
"keepAspectRatio": "Keep aspect ratio",
|
||||||
|
"loadingPreview": "Loading Image Preview...",
|
||||||
|
"minHeightError": "The minimum valid height is 100px.",
|
||||||
|
"minWidthError": "The minimum valid width is 100px.",
|
||||||
|
"showAnnotations": "Show Annotations"
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import Header from './Header.json';
|
|||||||
import MeasurementTable from './MeasurementTable.json';
|
import MeasurementTable from './MeasurementTable.json';
|
||||||
import StudyList from './StudyList.json';
|
import StudyList from './StudyList.json';
|
||||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
|
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'en-US': {
|
'en-US': {
|
||||||
@@ -19,5 +20,6 @@ export default {
|
|||||||
MeasurementTable,
|
MeasurementTable,
|
||||||
StudyList,
|
StudyList,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
|
ViewportDownloadForm,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -3,6 +3,28 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.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)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* download tool fixes & improvements ([#1235](https://github.com/OHIF/Viewers/issues/1235)) ([b9574b6](https://github.com/OHIF/Viewers/commit/b9574b6efcfeb85cde35b5cae63282f8e1b35be6))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [1.1.8](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.7...@ohif/ui@1.1.8) (2020-01-08)
|
## [1.1.8](https://github.com/OHIF/Viewers/compare/@ohif/ui@1.1.7...@ohif/ui@1.1.8) (2020-01-08)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/ui",
|
"name": "@ohif/ui",
|
||||||
"version": "1.1.8",
|
"version": "1.2.0",
|
||||||
"description": "A set of React components for Medical Imaging Viewers",
|
"description": "A set of React components for Medical Imaging Viewers",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import React, { useEffect, useState, createRef } from 'react';
|
import React, {
|
||||||
|
useRef,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
createRef,
|
||||||
|
} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import './ViewportDownloadForm.styl';
|
import './ViewportDownloadForm.styl';
|
||||||
import { TextInput, Select } from '@ohif/ui';
|
import { TextInput, Select, Icon } from '@ohif/ui';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
const FILE_TYPE_OPTIONS = [
|
const FILE_TYPE_OPTIONS = [
|
||||||
{
|
{
|
||||||
@@ -17,6 +24,7 @@ const FILE_TYPE_OPTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const DEFAULT_FILENAME = 'image';
|
const DEFAULT_FILENAME = 'image';
|
||||||
|
const REFRESH_VIEWPORT_TIMEOUT = 1000;
|
||||||
|
|
||||||
const ViewportDownloadForm = ({
|
const ViewportDownloadForm = ({
|
||||||
activeViewport,
|
activeViewport,
|
||||||
@@ -32,7 +40,7 @@ const ViewportDownloadForm = ({
|
|||||||
maximumSize,
|
maximumSize,
|
||||||
canvasClass,
|
canvasClass,
|
||||||
}) => {
|
}) => {
|
||||||
const [t] = useTranslation('Buttons');
|
const [t] = useTranslation('ViewportDownloadForm');
|
||||||
|
|
||||||
const [filename, setFilename] = useState(DEFAULT_FILENAME);
|
const [filename, setFilename] = useState(DEFAULT_FILENAME);
|
||||||
const [fileType, setFileType] = useState('jpg');
|
const [fileType, setFileType] = useState('jpg');
|
||||||
@@ -44,6 +52,12 @@ const ViewportDownloadForm = ({
|
|||||||
|
|
||||||
const [showAnnotations, setShowAnnotations] = useState(true);
|
const [showAnnotations, setShowAnnotations] = useState(true);
|
||||||
|
|
||||||
|
const [keepAspect, setKeepAspect] = useState(true);
|
||||||
|
const [aspectMultiplier, setAspectMultiplier] = useState({
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
});
|
||||||
|
|
||||||
const [viewportElement, setViewportElement] = useState();
|
const [viewportElement, setViewportElement] = useState();
|
||||||
const [viewportElementDimensions, setViewportElementDimensions] = useState({
|
const [viewportElementDimensions, setViewportElementDimensions] = useState({
|
||||||
width: defaultSize,
|
width: defaultSize,
|
||||||
@@ -62,41 +76,129 @@ const ViewportDownloadForm = ({
|
|||||||
height: defaultSize,
|
height: defaultSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cornerstone's `enable/disable`
|
const [error, setError] = useState({
|
||||||
useEffect(() => {
|
width: false,
|
||||||
enableViewport(viewportElement);
|
height: false,
|
||||||
|
filename: false,
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
const hasError = Object.values(error).includes(true);
|
||||||
disableViewport(viewportElement);
|
|
||||||
};
|
|
||||||
}, [disableViewport, enableViewport, viewportElement]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const refreshViewport = useRef(null);
|
||||||
const { width, height } = viewportElementDimensions;
|
|
||||||
const validSize = value => (value >= minimumSize ? value : minimumSize);
|
|
||||||
const loadAndUpdateViewports = async () => {
|
|
||||||
await loadImage(activeViewport, viewportElement, width, height);
|
|
||||||
toggleAnnotations(showAnnotations, viewportElement);
|
|
||||||
|
|
||||||
const {
|
const downloadImage = () => {
|
||||||
dataUrl,
|
downloadBlob(
|
||||||
width: viewportElementWidth,
|
filename || DEFAULT_FILENAME,
|
||||||
height: viewportElementHeight,
|
fileType,
|
||||||
} = await updateViewportPreview(
|
viewportElement,
|
||||||
viewportElement,
|
downloadCanvas.ref.current
|
||||||
downloadCanvas.ref.current,
|
);
|
||||||
fileType
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} event - Input change event
|
||||||
|
* @param {string} dimension - "height" | "width"
|
||||||
|
*/
|
||||||
|
const onDimensionsChange = (event, dimension) => {
|
||||||
|
const oppositeDimension = dimension === 'height' ? 'width' : 'height';
|
||||||
|
const sanitizedTargetValue = event.target.value.replace(/\D/, '');
|
||||||
|
const isEmpty = sanitizedTargetValue === '';
|
||||||
|
const newDimensions = { ...dimensions };
|
||||||
|
const updatedDimension = isEmpty
|
||||||
|
? ''
|
||||||
|
: Math.min(sanitizedTargetValue, maximumSize);
|
||||||
|
|
||||||
|
if (updatedDimension === dimensions[dimension]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newDimensions[dimension] = updatedDimension;
|
||||||
|
|
||||||
|
if (keepAspect && newDimensions[oppositeDimension] !== '') {
|
||||||
|
newDimensions[oppositeDimension] = Math.round(
|
||||||
|
newDimensions[dimension] * aspectMultiplier[oppositeDimension]
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setViewportPreview(state => ({
|
// In current code, keepAspect is always `true`
|
||||||
|
// And we always start w/ a square width/height
|
||||||
|
setDimensions(newDimensions);
|
||||||
|
|
||||||
|
// Only update if value is non-empty
|
||||||
|
if (!isEmpty) {
|
||||||
|
setViewportElementDimensions(newDimensions);
|
||||||
|
setDownloadCanvas(state => ({
|
||||||
...state,
|
...state,
|
||||||
src: dataUrl,
|
...newDimensions,
|
||||||
width: validSize(viewportElementWidth),
|
|
||||||
height: validSize(viewportElementHeight),
|
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const error_messages = {
|
||||||
|
width: t('minWidthError'),
|
||||||
|
height: t('minHeightError'),
|
||||||
|
filename: t('emptyFilenameError'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderErrorHandler = errorType => {
|
||||||
|
if (!error[errorType]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="input-error">{error_messages[errorType]}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeepAspectToggle = () => {
|
||||||
|
const { width, height } = dimensions;
|
||||||
|
const aspectMultiplier = { ...aspectMultiplier };
|
||||||
|
if (!keepAspect) {
|
||||||
|
const base = Math.min(width, height);
|
||||||
|
aspectMultiplier.width = width / base;
|
||||||
|
aspectMultiplier.height = height / base;
|
||||||
|
setAspectMultiplier(aspectMultiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
setKeepAspect(!keepAspect);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validSize = value => (value >= minimumSize ? value : minimumSize);
|
||||||
|
const loadAndUpdateViewports = useCallback(async () => {
|
||||||
|
const { width: scaledWidth, height: scaledHeight } = await loadImage(
|
||||||
|
activeViewport,
|
||||||
|
viewportElement,
|
||||||
|
dimensions.width,
|
||||||
|
dimensions.height
|
||||||
|
);
|
||||||
|
|
||||||
|
toggleAnnotations(showAnnotations, viewportElement);
|
||||||
|
|
||||||
|
const scaledDimensions = {
|
||||||
|
height: validSize(scaledHeight),
|
||||||
|
width: validSize(scaledWidth),
|
||||||
};
|
};
|
||||||
|
|
||||||
loadAndUpdateViewports();
|
setViewportElementDimensions(scaledDimensions);
|
||||||
|
setDownloadCanvas(state => ({
|
||||||
|
...state,
|
||||||
|
...scaledDimensions,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
dataUrl,
|
||||||
|
width: viewportElementWidth,
|
||||||
|
height: viewportElementHeight,
|
||||||
|
} = await updateViewportPreview(
|
||||||
|
viewportElement,
|
||||||
|
downloadCanvas.ref.current,
|
||||||
|
fileType
|
||||||
|
);
|
||||||
|
|
||||||
|
setViewportPreview(state => ({
|
||||||
|
...state,
|
||||||
|
src: dataUrl,
|
||||||
|
width: validSize(viewportElementWidth),
|
||||||
|
height: validSize(viewportElementHeight),
|
||||||
|
}));
|
||||||
}, [
|
}, [
|
||||||
activeViewport,
|
activeViewport,
|
||||||
viewportElement,
|
viewportElement,
|
||||||
@@ -111,76 +213,96 @@ const ViewportDownloadForm = ({
|
|||||||
viewportElementDimensions,
|
viewportElementDimensions,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
useEffect(() => {
|
||||||
* @param {object} event - Input change event
|
enableViewport(viewportElement);
|
||||||
* @param {string} dimension - "height" | "width"
|
|
||||||
*/
|
|
||||||
const onDimensionsChange = (event, dimension) => {
|
|
||||||
const sanitizedTargetValue = event.target.value.replace(/\D/, '');
|
|
||||||
const isEmpty = sanitizedTargetValue === '';
|
|
||||||
const updatedDimension = isEmpty
|
|
||||||
? ''
|
|
||||||
: Math.min(sanitizedTargetValue, maximumSize);
|
|
||||||
|
|
||||||
if (updatedDimension === dimensions.width) {
|
return () => {
|
||||||
return;
|
disableViewport(viewportElement);
|
||||||
|
};
|
||||||
|
}, [disableViewport, enableViewport, viewportElement]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (refreshViewport.current !== null) {
|
||||||
|
clearTimeout(refreshViewport.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
// In current code, keepAspect is always `true`
|
refreshViewport.current = setTimeout(() => {
|
||||||
// And we always start w/ a square width/height
|
refreshViewport.current = null;
|
||||||
setDimensions({
|
loadAndUpdateViewports();
|
||||||
width: updatedDimension,
|
}, REFRESH_VIEWPORT_TIMEOUT);
|
||||||
height: updatedDimension,
|
}, [
|
||||||
});
|
activeViewport,
|
||||||
|
viewportElement,
|
||||||
|
showAnnotations,
|
||||||
|
dimensions,
|
||||||
|
loadImage,
|
||||||
|
toggleAnnotations,
|
||||||
|
updateViewportPreview,
|
||||||
|
fileType,
|
||||||
|
downloadCanvas.ref,
|
||||||
|
minimumSize,
|
||||||
|
maximumSize,
|
||||||
|
]);
|
||||||
|
|
||||||
// Only update if value is non-empty
|
useEffect(() => {
|
||||||
if (!isEmpty) {
|
const { width, height } = dimensions;
|
||||||
setViewportElementDimensions({
|
const hasError = {
|
||||||
height: updatedDimension,
|
width: width < minimumSize,
|
||||||
width: updatedDimension,
|
height: height < minimumSize,
|
||||||
});
|
filename: !filename,
|
||||||
setDownloadCanvas(state => ({
|
};
|
||||||
...state,
|
|
||||||
height: updatedDimension,
|
|
||||||
width: updatedDimension,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const downloadImage = () => {
|
setError({ ...hasError });
|
||||||
downloadBlob(
|
}, [dimensions, filename, minimumSize]);
|
||||||
filename || DEFAULT_FILENAME,
|
|
||||||
fileType,
|
|
||||||
viewportElement,
|
|
||||||
downloadCanvas.ref.current
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ViewportDownloadForm">
|
<div className="ViewportDownloadForm">
|
||||||
<div className="title">
|
<div className="title">{t('formTitle')}</div>
|
||||||
{t(
|
|
||||||
'Please specify the dimensions, filename, and desired type for the output image.'
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="file-info-container" data-cy="file-info-container">
|
<div className="file-info-container" data-cy="file-info-container">
|
||||||
<div className="col">
|
<div className="dimension-wrapper">
|
||||||
<div className="width">
|
<div className="dimensions">
|
||||||
<TextInput
|
<div className="width">
|
||||||
data-cy="image-width"
|
<TextInput
|
||||||
value={dimensions.width}
|
type="number"
|
||||||
label={t('Image width (px)')}
|
min={minimumSize}
|
||||||
onChange={evt => onDimensionsChange(evt, 'height')}
|
max={maximumSize}
|
||||||
/>
|
value={dimensions.width}
|
||||||
|
label={t('imageWidth')}
|
||||||
|
onChange={evt => onDimensionsChange(evt, 'width')}
|
||||||
|
data-cy="image-width"
|
||||||
|
/>
|
||||||
|
{renderErrorHandler('width')}
|
||||||
|
</div>
|
||||||
|
<div className="height">
|
||||||
|
<TextInput
|
||||||
|
type="number"
|
||||||
|
min={minimumSize}
|
||||||
|
max={maximumSize}
|
||||||
|
value={dimensions.height}
|
||||||
|
label={t('imageHeight')}
|
||||||
|
onChange={evt => onDimensionsChange(evt, 'height')}
|
||||||
|
data-cy="image-height"
|
||||||
|
/>
|
||||||
|
{renderErrorHandler('height')}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="height">
|
<div className="keep-aspect-wrapper">
|
||||||
<TextInput
|
<button
|
||||||
data-cy="image-height"
|
id="keep-aspect"
|
||||||
value={dimensions.height}
|
className={classnames(
|
||||||
label={t('Image height (px)')}
|
'form-button btn',
|
||||||
onChange={evt => onDimensionsChange(evt, 'width')}
|
keepAspect ? 'active' : ''
|
||||||
/>
|
)}
|
||||||
|
data-cy="keep-aspect"
|
||||||
|
alt={t('keepAspectRatio')}
|
||||||
|
onClick={onKeepAspectToggle}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={keepAspect ? 'link' : 'unlink'}
|
||||||
|
alt={keepAspect ? 'Dismiss Aspect' : 'Keep Aspect'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -191,9 +313,10 @@ const ViewportDownloadForm = ({
|
|||||||
data-cy="file-name"
|
data-cy="file-name"
|
||||||
value={filename}
|
value={filename}
|
||||||
onChange={event => setFilename(event.target.value)}
|
onChange={event => setFilename(event.target.value)}
|
||||||
label={t('File name')}
|
label={t('filename')}
|
||||||
id="file-name"
|
id="file-name"
|
||||||
/>
|
/>
|
||||||
|
{renderErrorHandler('filename')}
|
||||||
</div>
|
</div>
|
||||||
<div className="file-type">
|
<div className="file-type">
|
||||||
<Select
|
<Select
|
||||||
@@ -201,7 +324,7 @@ const ViewportDownloadForm = ({
|
|||||||
data-cy="file-type"
|
data-cy="file-type"
|
||||||
onChange={event => setFileType(event.target.value)}
|
onChange={event => setFileType(event.target.value)}
|
||||||
options={FILE_TYPE_OPTIONS}
|
options={FILE_TYPE_OPTIONS}
|
||||||
label={t('File type')}
|
label={t('fileType')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,7 +340,7 @@ const ViewportDownloadForm = ({
|
|||||||
checked={showAnnotations}
|
checked={showAnnotations}
|
||||||
onChange={event => setShowAnnotations(event.target.checked)}
|
onChange={event => setShowAnnotations(event.target.checked)}
|
||||||
/>
|
/>
|
||||||
{t('Show Annotations')}
|
{t('showAnnotations')}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -245,15 +368,23 @@ const ViewportDownloadForm = ({
|
|||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="preview" data-cy="image-preview">
|
{viewportPreview.src ? (
|
||||||
<h4> {t('Image Preview')}</h4>
|
<div className="preview" data-cy="image-preview">
|
||||||
<img
|
<div className="preview-header"> {t('imagePreview')}</div>
|
||||||
className="viewport-preview"
|
<img
|
||||||
src={viewportPreview.src}
|
className="viewport-preview"
|
||||||
alt="Viewport Preview"
|
src={viewportPreview.src}
|
||||||
data-cy="viewport-preview-img"
|
alt={t('imagePreview')}
|
||||||
/>
|
data-cy="image-preview"
|
||||||
</div>
|
data-cy="viewport-preview-img"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="loading-image">
|
||||||
|
<Icon name="circle-notch" className="icon-spin" />
|
||||||
|
{t('loadingPreview')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
<div className="action-cancel">
|
<div className="action-cancel">
|
||||||
@@ -263,16 +394,17 @@ const ViewportDownloadForm = ({
|
|||||||
className="btn btn-danger"
|
className="btn btn-danger"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
{t('Cancel')}
|
{t('Buttons:Cancel')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="action-save">
|
<div className="action-save">
|
||||||
<button
|
<button
|
||||||
|
disabled={hasError}
|
||||||
onClick={downloadImage}
|
onClick={downloadImage}
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
data-cy="download-btn"
|
data-cy="download-btn"
|
||||||
>
|
>
|
||||||
{t('Download')}
|
{t('Buttons:Download')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
input, select
|
input, select
|
||||||
max-height: 30px;
|
max-height: 30px;
|
||||||
|
|
||||||
|
#keep-aspect svg
|
||||||
|
margin-top: 3px;
|
||||||
|
|
||||||
.title
|
.title
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -17,85 +20,117 @@
|
|||||||
.file-info-container
|
.file-info-container
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 20px 10px 0;
|
padding: 20px 10px;
|
||||||
background-color: #16202b;
|
background-color: var(--ui-gray-dark);
|
||||||
|
|
||||||
.form-control.input-ohif
|
|
||||||
padding: 6px 12px;
|
|
||||||
|
|
||||||
@media screen and (max-width: 1023px)
|
@media screen and (max-width: 1023px)
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
.col
|
.width,
|
||||||
flex-grow: 1;
|
.height,
|
||||||
|
.file-name,
|
||||||
|
.file-type
|
||||||
|
height: 56px;
|
||||||
|
|
||||||
.input-ohif
|
.input-ohif
|
||||||
margin-left: 15px;
|
margin: 0 5px;
|
||||||
|
|
||||||
|
.file-name,
|
||||||
|
.file-type
|
||||||
|
.select-ohif, .input-ohif
|
||||||
|
width: 170px;
|
||||||
|
|
||||||
|
.input-ohif-label, .select-ohif-label
|
||||||
|
width: 90px;
|
||||||
|
display: inline-block;
|
||||||
@media screen and (max-width: 1023px)
|
@media screen and (max-width: 1023px)
|
||||||
margin-left: 0;
|
width: 120px;
|
||||||
margin-top: 5px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.file-type
|
.dimension-wrapper
|
||||||
.select-ohif
|
display: flex;
|
||||||
margin-left: 17px;
|
flex-direction: row;
|
||||||
|
|
||||||
|
.dimensions
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.input-ohif-label
|
||||||
|
width: 120px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
.input-ohif
|
||||||
@media screen and (max-width: 1023px)
|
@media screen and (max-width: 1023px)
|
||||||
margin-left: 0;
|
width: 170px;
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.show-annotations
|
.keep-aspect-wrapper
|
||||||
font-weight: bold;
|
display: flex;
|
||||||
line-height: 30px;
|
justify-content: center;
|
||||||
input
|
align-items: center;
|
||||||
margin-right: 7px;
|
padding: 0 10px;
|
||||||
vertical-align: middle;
|
height: 86px;
|
||||||
label
|
|
||||||
display: flex;
|
.show-annotations
|
||||||
justify-content: center;
|
font-weight: bold;
|
||||||
align-items: center;
|
line-height: 30px;
|
||||||
|
input
|
||||||
|
margin-right: 7px;
|
||||||
|
vertical-align: middle;
|
||||||
|
label
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.loading-image
|
||||||
|
height: 580px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--active-color);
|
||||||
|
font-size: 20px;
|
||||||
|
|
||||||
|
.icon-spin
|
||||||
|
margin-right: 15px;
|
||||||
|
|
||||||
.preview
|
.preview
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction column;
|
flex-direction: column;
|
||||||
height: fit-content;
|
background-color: var(--ui-gray-dark);
|
||||||
background-color: #16202b;
|
|
||||||
width: fit-content;
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
align-self: center;
|
justify-content: flex-start;
|
||||||
margin-bottom: 20px;
|
align-items: center;
|
||||||
@media screen and (max-width: 1023px)
|
height: 580px;
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.viewport-preview
|
.viewport-preview
|
||||||
max-height: 512px;
|
max-height: 512px;
|
||||||
max-width: 512px;
|
max-width: 512px;
|
||||||
|
|
||||||
h4
|
.preview-header
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align center;
|
text-align center;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
|
|
||||||
.preview-container
|
|
||||||
width: auto;
|
|
||||||
height: 100%;
|
|
||||||
max-height: 400px;
|
|
||||||
object-fit contain;
|
|
||||||
|
|
||||||
.actions
|
.actions
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
.action-cancel
|
margin-top: 20px;
|
||||||
margin: 0 20px;
|
|
||||||
.actions-save
|
.btn
|
||||||
margin: 0 0 0 10px;
|
margin: 0 10px;
|
||||||
|
|
||||||
|
.input-error
|
||||||
|
font-size: 12px;
|
||||||
|
color: red;
|
||||||
|
text-align: center;
|
||||||
|
margin: 3px 0;
|
||||||
|
|
||||||
|
|
||||||
.modal-dialog
|
.modal-dialog
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -184,10 +184,12 @@ class MeasurementTable extends Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
getTimepointsHeader = () => {
|
getTimepointsHeader = () => {
|
||||||
return this.props.timepoints.map((timepoint, index) => {
|
const { timepoints, t } = this.props;
|
||||||
|
|
||||||
|
return timepoints.map((timepoint, index) => {
|
||||||
return (
|
return (
|
||||||
<div key={index} className="measurementTableHeaderItem">
|
<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 className="timepointDate">{timepoint.date}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -84,7 +84,6 @@
|
|||||||
position: relative
|
position: relative
|
||||||
top: 2px
|
top: 2px
|
||||||
|
|
||||||
|
|
||||||
.tooltip-warning
|
.tooltip-warning
|
||||||
&.in
|
&.in
|
||||||
opacity 1
|
opacity 1
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ class MeasurementTableItem extends Component {
|
|||||||
<div>{this.getTableListItem()}</div>
|
<div>{this.getTableListItem()}</div>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment>{this.getTableListItem()}</React.Fragment>
|
<React.Fragment>{this.getTableListItem()}</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ class MeasurementTableItem extends Component {
|
|||||||
nsSeparator: '|',
|
nsSeparator: '|',
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div>{this.getDataDisplayText()}</div>
|
<div className="displayTexts">{this.getDataDisplayText()}</div>
|
||||||
<div className="rowActions">{actionButtons}</div>
|
<div className="rowActions">{actionButtons}</div>
|
||||||
</div>
|
</div>
|
||||||
</TableListItem>
|
</TableListItem>
|
||||||
|
|||||||
@@ -25,6 +25,20 @@ $headerRowHeight = 63px
|
|||||||
text-align: left
|
text-align: left
|
||||||
flex: 1;
|
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
|
.numberOfItems
|
||||||
color: var(--ui-sky-blue)
|
color: var(--ui-sky-blue)
|
||||||
float: right
|
float: right
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ export class TableListItem extends Component {
|
|||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
itemClass: PropTypes.string,
|
itemClass: PropTypes.string,
|
||||||
itemIndex: PropTypes.number,
|
itemIndex: PropTypes.number,
|
||||||
itemKey: PropTypes.oneOfType(['number', 'string']),
|
itemKey: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
]),
|
||||||
onItemClick: PropTypes.func.isRequired,
|
onItemClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
.icon-pulse
|
.icon-pulse
|
||||||
fa-spin 1s infinite steps(8)
|
fa-spin 1s infinite steps(8)
|
||||||
|
|
||||||
|
.icon-spin {
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes fa-spin{
|
@keyframes fa-spin{
|
||||||
0%{ transform:rotate(0deg) }
|
0%{ transform:rotate(0deg) }
|
||||||
to{ transform:rotate(1turn) }
|
to{ transform:rotate(1turn) }
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ import thLarge from './icons/th-large.svg';
|
|||||||
import thList from './icons/th-list.svg';
|
import thList from './icons/th-list.svg';
|
||||||
import times from './icons/times.svg';
|
import times from './icons/times.svg';
|
||||||
import trash from './icons/trash.svg';
|
import trash from './icons/trash.svg';
|
||||||
|
import unlink from './icons/unlink.svg';
|
||||||
import user from './icons/user.svg';
|
import user from './icons/user.svg';
|
||||||
import youtube from './icons/youtube.svg';
|
import youtube from './icons/youtube.svg';
|
||||||
|
|
||||||
@@ -158,6 +159,7 @@ const ICONS = {
|
|||||||
rotate,
|
rotate,
|
||||||
'rotate-right': rotateRight,
|
'rotate-right': rotateRight,
|
||||||
trash,
|
trash,
|
||||||
|
unlink,
|
||||||
'exclamation-circle': exclamationCircle,
|
'exclamation-circle': exclamationCircle,
|
||||||
link,
|
link,
|
||||||
'exclamation-triangle': exclamationTriangle,
|
'exclamation-triangle': exclamationTriangle,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
aria-labelledby="unlink"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<title id="title">Unlink</title>
|
||||||
|
<path d="M304.083 388.936c4.686 4.686 4.686 12.284 0 16.971l-65.057 65.056c-54.709 54.711-143.27 54.721-197.989 0-54.713-54.713-54.719-143.27 0-197.989l65.056-65.057c4.686-4.686 12.284-4.686 16.971 0l22.627 22.627c4.686 4.686 4.686 12.284 0 16.971L81.386 311.82c-34.341 34.341-33.451 88.269.597 120.866 32.577 31.187 84.788 31.337 117.445-1.32l65.057-65.056c4.686-4.686 12.284-4.686 16.971 0l22.627 22.626zm-56.568-243.245l64.304-64.304c34.346-34.346 88.286-33.453 120.882.612 31.18 32.586 31.309 84.785-1.335 117.43l-65.056 65.057c-4.686 4.686-4.686 12.284 0 16.971l22.627 22.627c4.686 4.686 12.284 4.686 16.971 0l65.056-65.057c54.711-54.709 54.721-143.271 0-197.99-54.71-54.711-143.27-54.72-197.989 0l-65.057 65.057c-4.686 4.686-4.686 12.284 0 16.971l22.627 22.627c4.685 4.685 12.283 4.685 16.97-.001zm238.343 362.794l22.627-22.627c4.686-4.686 4.686-12.284 0-16.971L43.112 3.515c-4.686-4.686-12.284-4.686-16.971 0L3.515 26.142c-4.686 4.686-4.686 12.284 0 16.971l465.373 465.373c4.686 4.686 12.284 4.686 16.97-.001z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -29,18 +29,18 @@ class Select extends Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="select-ohif-container">
|
<div className="select-ohif-container">
|
||||||
<label className="select-ohif-label" htmlFor={this.id}>
|
{this.props.label && (
|
||||||
{this.props.label}
|
<label className="select-ohif-label" htmlFor={this.id}>{this.props.label}</label>
|
||||||
<select className="form-control select-ohif" {...this.props}>
|
)}
|
||||||
{this.props.options.map(({ key, value }) => {
|
<select className="form-control select-ohif" {...this.props}>
|
||||||
return (
|
{this.props.options.map(({ key, value }) => {
|
||||||
<option key={key} value={value}>
|
return (
|
||||||
{key}
|
<option key={key} value={value}>
|
||||||
</option>
|
{key}
|
||||||
);
|
</option>
|
||||||
})}
|
);
|
||||||
</select>
|
})}
|
||||||
</label>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class TextInput extends React.Component {
|
|||||||
PropTypes.number
|
PropTypes.number
|
||||||
]),
|
]),
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
label:PropTypes.string,
|
label: PropTypes.string,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,15 +28,15 @@ class TextInput extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="input-ohif-container">
|
<div className="input-ohif-container">
|
||||||
<label className="input-ohif-label" htmlFor={this.props.id}>
|
{this.props.label && (
|
||||||
{this.props.label}
|
<label className="input-ohif-label" htmlFor={this.props.id}>{this.props.label}</label>
|
||||||
<input
|
)}
|
||||||
type={this.props.type}
|
<input
|
||||||
id={this.props.id}
|
type={this.props.type}
|
||||||
className="form-control input-ohif"
|
id={this.props.id}
|
||||||
{...this.props}
|
className="form-control input-ohif"
|
||||||
/>
|
{...this.props}
|
||||||
</label>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
|
|||||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
const ExtractCssChunksPlugin = require('extract-css-chunks-webpack-plugin');
|
const ExtractCssChunksPlugin = require('extract-css-chunks-webpack-plugin');
|
||||||
// const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const { InjectManifest } = require('workbox-webpack-plugin');
|
const { InjectManifest } = require('workbox-webpack-plugin');
|
||||||
const TerserJSPlugin = require('terser-webpack-plugin');
|
const TerserJSPlugin = require('terser-webpack-plugin');
|
||||||
@@ -27,6 +26,7 @@ const PUBLIC_URL = process.env.PUBLIC_URL || '/';
|
|||||||
const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js';
|
const APP_CONFIG = process.env.APP_CONFIG || 'config/default.js';
|
||||||
const PROXY_TARGET = process.env.PROXY_TARGET;
|
const PROXY_TARGET = process.env.PROXY_TARGET;
|
||||||
const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
|
const PROXY_DOMAIN = process.env.PROXY_DOMAIN;
|
||||||
|
const ENTRY_TARGET = process.env.ENTRY_TARGET || `${SRC_DIR}/index.js`;
|
||||||
|
|
||||||
module.exports = (env, argv) => {
|
module.exports = (env, argv) => {
|
||||||
const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR });
|
const baseConfig = webpackBase(env, argv, { SRC_DIR, DIST_DIR });
|
||||||
@@ -34,6 +34,9 @@ module.exports = (env, argv) => {
|
|||||||
const hasProxy = PROXY_TARGET && PROXY_DOMAIN;
|
const hasProxy = PROXY_TARGET && PROXY_DOMAIN;
|
||||||
|
|
||||||
const mergedConfig = merge(baseConfig, {
|
const mergedConfig = merge(baseConfig, {
|
||||||
|
entry: {
|
||||||
|
app: ENTRY_TARGET,
|
||||||
|
},
|
||||||
output: {
|
output: {
|
||||||
path: DIST_DIR,
|
path: DIST_DIR,
|
||||||
filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
|
filename: isProdBuild ? '[name].bundle.[chunkhash].js' : '[name].js',
|
||||||
|
|||||||
@@ -3,6 +3,87 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [3.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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.4.12](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.11...@ohif/viewer@3.4.12) (2020-01-30)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.4.11](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.10...@ohif/viewer@3.4.11) (2020-01-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* download tool fixes & improvements ([#1235](https://github.com/OHIF/Viewers/issues/1235)) ([b9574b6](https://github.com/OHIF/Viewers/commit/b9574b6efcfeb85cde35b5cae63282f8e1b35be6))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [3.4.10](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.9...@ohif/viewer@3.4.10) (2020-01-28)
|
||||||
|
|
||||||
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [3.4.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.8...@ohif/viewer@3.4.9) (2020-01-28)
|
## [3.4.9](https://github.com/OHIF/Viewers/compare/@ohif/viewer@3.4.8...@ohif/viewer@3.4.9) (2020-01-28)
|
||||||
|
|
||||||
**Note:** Version bump only for package @ohif/viewer
|
**Note:** Version bump only for package @ohif/viewer
|
||||||
|
|||||||
Binary file not shown.
@@ -16,14 +16,14 @@ describe('OHIF Download Snapshot File', () => {
|
|||||||
.click();
|
.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks displayed information for Tablet experience', function() {
|
it('checks displayed information for Tablet experience', function () {
|
||||||
// Set Tablet resolution
|
// Set Tablet resolution
|
||||||
cy.viewport(1000, 660);
|
cy.viewport(1000, 660);
|
||||||
// Visual comparison
|
// Visual comparison
|
||||||
cy.screenshot('Download Image Modal - Tablet experience');
|
cy.screenshot('Download Image Modal - Tablet experience');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks displayed information for Desktop experience', function() {
|
it('checks displayed information for Desktop experience', function () {
|
||||||
// Set Desktop resolution
|
// Set Desktop resolution
|
||||||
cy.viewport(1750, 720);
|
cy.viewport(1750, 720);
|
||||||
// Visual comparison
|
// Visual comparison
|
||||||
@@ -52,7 +52,7 @@ describe('OHIF Download Snapshot File', () => {
|
|||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cancel changes on download modal', function() {
|
it('cancel changes on download modal', function () {
|
||||||
//Change Image Width, Filename and File Type
|
//Change Image Width, Filename and File Type
|
||||||
cy.get('[data-cy="image-width"]')
|
cy.get('[data-cy="image-width"]')
|
||||||
.clear()
|
.clear()
|
||||||
@@ -91,7 +91,7 @@ describe('OHIF Download Snapshot File', () => {
|
|||||||
// //Check error message
|
// //Check error message
|
||||||
// });
|
// });
|
||||||
|
|
||||||
it('checks if "Show Annotations" checkbox will display annotations', function() {
|
it('checks if "Show Annotations" checkbox will display annotations', function () {
|
||||||
// Close modal that is initially opened
|
// Close modal that is initially opened
|
||||||
cy.get('[data-cy="close-button"]').click();
|
cy.get('[data-cy="close-button"]').click();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
describe('OHIF Load Local File', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/local');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('checks if user can navigate to /local', () => {
|
||||||
|
cy.get('.drag-drop-contents').should(
|
||||||
|
'contain',
|
||||||
|
'Drag and Drop DICOM files here to load them in the Viewer'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads an invalid file and verify if viewer is empty', () => {
|
||||||
|
const fileName = 'example.json';
|
||||||
|
|
||||||
|
cy.fixture(fileName).then(fileContent => {
|
||||||
|
cy.get('input[type=file]')
|
||||||
|
.first()
|
||||||
|
.upload({
|
||||||
|
fileContent,
|
||||||
|
fileName,
|
||||||
|
mimeType: 'application/json',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//Verify if there is no thumbnail visible
|
||||||
|
cy.expectMinimumThumbnails(0);
|
||||||
|
//Verify if DOC thumnails is displayed and load it on viewport
|
||||||
|
cy.get('[data-cy="thumbnail-list"]').should('not.exist');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
describe('OHIF Load Local PDF File', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('/local');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads a PDF DICOM file and visualize it', () => {
|
||||||
|
const fileName = 'PDFDICOMfile.pdf';
|
||||||
|
|
||||||
|
cy.fixture(fileName).then(fileContent => {
|
||||||
|
cy.get('input[type=file]')
|
||||||
|
.first()
|
||||||
|
.upload({
|
||||||
|
fileContent,
|
||||||
|
fileName,
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//Verify if 1 thumbnail is visible
|
||||||
|
cy.expectMinimumThumbnails(1);
|
||||||
|
//Verify if DOC thumnails is displayed and load it on viewport
|
||||||
|
cy.get('[data-cy="thumbnail-list"]')
|
||||||
|
.contains('DOC')
|
||||||
|
.click({ force: true });
|
||||||
|
|
||||||
|
//Verify if PDF document is displayed on the viewport
|
||||||
|
cy.get('#pdf-canvas-container').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import '@percy/cypress';
|
import '@percy/cypress';
|
||||||
|
import 'cypress-file-upload';
|
||||||
import { DragSimulator } from '../helpers/DragSimulator.js';
|
import { DragSimulator } from '../helpers/DragSimulator.js';
|
||||||
import {
|
import {
|
||||||
initCornerstoneToolsAliases,
|
initCornerstoneToolsAliases,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@ohif/viewer",
|
"name": "@ohif/viewer",
|
||||||
"version": "3.4.9",
|
"version": "3.6.1",
|
||||||
"description": "OHIF Viewer",
|
"description": "OHIF Viewer",
|
||||||
"author": "OHIF Contributors",
|
"author": "OHIF Contributors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -19,12 +19,14 @@
|
|||||||
"scripts": {
|
"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: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": "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: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: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",
|
"build:viewer:package": "yarn run build:package",
|
||||||
"dev": "cross-env NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
|
"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: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": "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",
|
"start": "yarn run dev",
|
||||||
"test:e2e": "cypress open",
|
"test:e2e": "cypress open",
|
||||||
"test:e2e:ci": "percy exec -- cypress run --config video=false --record --browser chrome --spec 'cypress/integration/visual-regression/**/*'",
|
"test:e2e:ci": "percy exec -- cypress run --config video=false --record --browser chrome --spec 'cypress/integration/visual-regression/**/*'",
|
||||||
@@ -45,14 +47,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.5.5",
|
"@babel/runtime": "^7.5.5",
|
||||||
"@ohif/core": "^2.3.6",
|
"@ohif/core": "^2.4.0",
|
||||||
"@ohif/extension-cornerstone": "^2.2.2",
|
"@ohif/extension-cornerstone": "^2.3.0",
|
||||||
"@ohif/extension-dicom-html": "^1.1.0",
|
"@ohif/extension-dicom-html": "^1.1.0",
|
||||||
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
||||||
"@ohif/extension-dicom-pdf": "^1.0.1",
|
"@ohif/extension-dicom-pdf": "^1.0.1",
|
||||||
"@ohif/extension-vtk": "^1.2.11",
|
"@ohif/extension-lesion-tracker": "^0.2.0",
|
||||||
"@ohif/i18n": "^0.52.4",
|
"@ohif/extension-vtk": "^1.2.17",
|
||||||
"@ohif/ui": "^1.1.8",
|
"@ohif/i18n": "^0.52.5",
|
||||||
|
"@ohif/ui": "^1.2.0",
|
||||||
"@tanem/react-nprogress": "^1.1.25",
|
"@tanem/react-nprogress": "^1.1.25",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.2.1",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
|
MeasurementService,
|
||||||
utils,
|
utils,
|
||||||
redux as reduxOHIF,
|
redux as reduxOHIF,
|
||||||
} from '@ohif/core';
|
} from '@ohif/core';
|
||||||
@@ -122,7 +123,12 @@ class App extends Component {
|
|||||||
} = this._appConfig;
|
} = this._appConfig;
|
||||||
|
|
||||||
this.initUserManager(oidc);
|
this.initUserManager(oidc);
|
||||||
_initServices([UINotificationService, UIModalService, UIDialogService]);
|
_initServices([
|
||||||
|
UINotificationService,
|
||||||
|
UIModalService,
|
||||||
|
UIDialogService,
|
||||||
|
MeasurementService,
|
||||||
|
]);
|
||||||
_initExtensions(
|
_initExtensions(
|
||||||
[...defaultExtensions, ...extensions],
|
[...defaultExtensions, ...extensions],
|
||||||
cornerstoneExtensionConfig,
|
cornerstoneExtensionConfig,
|
||||||
@@ -144,6 +150,7 @@ class App extends Component {
|
|||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIDialogService,
|
UIDialogService,
|
||||||
UIModalService,
|
UIModalService,
|
||||||
|
MeasurementService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
if (this._userManager) {
|
if (this._userManager) {
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ function ViewerRetrieveStudyData({
|
|||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error && !error.isCanceled) {
|
if (error && !error.isCanceled) {
|
||||||
setError(true);
|
setError(true);
|
||||||
|
log.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -290,11 +291,13 @@ function ViewerRetrieveStudyData({
|
|||||||
.catch(error => {
|
.catch(error => {
|
||||||
if (error && !error.isCanceled) {
|
if (error && !error.isCanceled) {
|
||||||
setError(true);
|
setError(true);
|
||||||
|
log.error(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
setError(true);
|
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'));
|
||||||
Reference in new issue
Block a user