Merge in feat/ui-v2-rebase-services

This commit is contained in:
James A. Petts 2020-06-11 17:26:38 +01:00
commit c5e6e97980
66 changed files with 3390 additions and 3355 deletions

View File

@ -195,69 +195,99 @@ const _connectToolsToMeasurementService = measurementService => {
const csToolsVer4MeasurementSource = _initMeasurementService(
measurementService
);
const { addOrUpdate } = csToolsVer4MeasurementSource;
const { addOrUpdate, remove } = csToolsVer4MeasurementSource;
const elementEnabledEvt = cornerstone.EVENTS.ELEMENT_ENABLED;
/* Measurement Service Events */
cornerstone.events.addEventListener(
cornerstone.EVENTS.ELEMENT_ENABLED,
event => {
// const {
// MEASUREMENT_ADDED,
// MEASUREMENT_UPDATED,
// } = measurementService.EVENTS;
cornerstone.events.addEventListener(elementEnabledEvt, evt => {
// TODO: Debounced update of measurements that are modified
// measurementService.subscribe(
// MEASUREMENT_ADDED,
// ({ source, measurement }) => {
// if (![sourceId].includes(source.id)) {
// const annotation = getAnnotation('Length', measurement.id);
function addMeasurement(csToolsEvent) {
console.log('CSTOOLS::addOrUpdate', csToolsEvent, csToolsEvent.detail);
try {
const evtDetail = csToolsEvent.detail;
const { toolName, toolType, measurementData } = evtDetail;
const csToolName = toolName || measurementData.toolType || toolType;
const measurementId = addOrUpdate(csToolName, evtDetail);
// 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 csToolName = toolName || measurementData.toolType || toolType;
csToolsAnnotation.id = measurementData._measurementServiceId;
addOrUpdate(csToolName, csToolsAnnotation);
} catch (error) {
console.warn('Failed to add or update measurement:', error);
if (measurementId) {
measurementData.id = measurementId;
}
};
[csTools.EVENTS.MEASUREMENT_ADDED].forEach(csToolsEvtName => {
event.detail.element.addEventListener(
csToolsEvtName,
({ detail: csToolsAnnotation }) => {
console.log(`csTools::${csToolsEvtName}`);
addOrUpdateMeasurement(csToolsAnnotation);
}
);
});
} catch (error) {
console.warn('Failed to add measurement:', error);
}
}
);
function updateMeasurement(csToolsEvent) {
try {
if (!csToolsEvent.detail.measurementData.id) {
return;
}
const evtDetail = csToolsEvent.detail;
const { toolName, toolType, measurementData } = evtDetail;
const csToolName = toolName || measurementData.toolType || toolType;
evtDetail.id = csToolsEvent.detail.measurementData.id;
addOrUpdate(csToolName, evtDetail);
//
} catch (error) {
console.warn('Failed to update measurement:', error);
}
}
function removeMeasurement(csToolsEvent) {
console.log('~~ removeEvt', csToolsEvent);
try {
if (csToolsEvent.detail.measurementData.id) {
remove(csToolsEvent.detail.measurementData.id);
}
} catch (error) {
console.warn('Failed to remove measurement:', error);
}
}
const enabledElement = evt.detail.element;
const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED;
const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED;
const removedEvt = csTools.EVENTS.MEASUREMENT_REMOVED;
enabledElement.addEventListener(completedEvt, addMeasurement);
enabledElement.addEventListener(updatedEvt, updateMeasurement);
enabledElement.addEventListener(removedEvt, removeMeasurement);
});
};
// 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);
// }
// }
// );

View File

@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui';
function ActionButtons() {
return (
<React.Fragment>
<ButtonGroup onClick={() => alert('Export')}>
<Button
className="px-2 py-2 text-base text-white bg-black border-primary-main"
size="initial"
color="inherit"
>
Export
</Button>
<IconButton
className="px-2 text-white bg-black border-primary-main"
color="inherit"
size="initial"
>
<Icon name="arrow-down" />
</IconButton>
</ButtonGroup>
<Button
className="px-2 py-2 ml-2 text-base text-white bg-black border border-primary-main"
variant="outlined"
size="initial"
color="inherit"
onClick={() => alert('Create Report')}
>
Create Report
</Button>
</React.Fragment>
);
}
export default ActionButtons;

View File

@ -1,73 +0,0 @@
import React from 'react';
import {
MeasurementsPanel,
Button,
ButtonGroup,
Icon,
IconButton,
} from '@ohif/ui';
export default function MeasurementTable({ servicesManager, commandsManager }) {
const { MeasurementService } = servicesManager.services;
console.log('MeasurementTable rendering!!!!!!!!!!!!!');
const actionButtons = (
<React.Fragment>
<ButtonGroup onClick={() => alert('Export')}>
<Button
className="text-white border-primary-main bg-black text-base py-2 px-2"
size="initial"
color="inherit"
>
Export
</Button>
<IconButton
className="bg-black border-primary-main px-2 text-white px-2"
color="inherit"
size="initial"
>
<Icon name="arrow-down" />
</IconButton>
</ButtonGroup>
<Button
className="text-white border border-primary-main bg-black text-base py-2 px-2 ml-2"
variant="outlined"
size="initial"
color="inherit"
onClick={() => alert('Create Report')}
>
Create Report
</Button>
</React.Fragment>
);
const descriptionData = {
date: '07-Sep-2010',
modality: 'CT',
description: 'CHEST/ABD/PELVIS W CONTRAST',
};
const activeMeasurementItem = 0;
const measurementTableData = {
title: 'Measurements',
amount: 10,
data: new Array(10).fill({}).map((el, i) => ({
id: i + 1,
label: 'Label short description',
displayText: '24.0 x 24.0 mm (S:4, I:22)',
isActive: activeMeasurementItem === i + 1,
})),
onClick: id => setActiveMeasurementItem(s => (s === id ? null : id)),
onEdit: id => alert(`Edit: ${id}`),
};
return (
<MeasurementsPanel
descriptionData={descriptionData}
measurementTableData={measurementTableData}
actionButtons={actionButtons}
/>
);
}

View File

@ -0,0 +1,55 @@
import React from 'react';
import { StudySummary, MeasurementTable } from '@ohif/ui';
import ActionButtons from './ActionButtons.jsx';
export default function PanelMeasurementTable({
servicesManager,
commandsManager,
}) {
const { MeasurementService } = servicesManager.services;
console.log('MeasurementTable rendering!!!!!!!!!!!!!');
const descriptionData = {
date: '07-Sep-2010',
modality: 'CT',
description: 'CHEST/ABD/PELVIS W CONTRAST',
};
const activeMeasurementItem = 0;
const measurementTableData = {
title: 'Measurements',
amount: 10,
data: new Array(10).fill({}).map((el, i) => ({
id: i + 1,
label: 'Label short description',
displayText: '24.0 x 24.0 mm (S:4, I:22)',
isActive: activeMeasurementItem === i + 1,
})),
onClick: id => setActiveMeasurementItem(s => (s === id ? null : id)),
onEdit: id => alert(`Edit: ${id}`),
};
return (
<>
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
<StudySummary
date={descriptionData.date}
modality={descriptionData.modality}
description={descriptionData.description}
/>
<MeasurementTable
title="Measurements"
amount={measurementTableData.data.length}
data={measurementTableData.data}
onClick={() => {}}
onEdit={id => alert(`Edit: ${id}`)}
/>
</div>
<div className="flex justify-center p-4">
<ActionButtons />
</div>
</>
);
}

View File

@ -3,15 +3,16 @@
import React from 'react';
import { useViewportGrid } from '@ohif/ui';
function getCommandsModule ({ servicesManager }) {
function getCommandsModule({ servicesManager }) {
const { UIDialogService } = servicesManager.services;
const definitions = {
toggleLayoutSelectionDialog: {
commandFn: () => {
if (!UIDialogService) {
window.alert('Unable to show dialog; no UI Dialog Service available.');
window.alert(
'Unable to show dialog; no UI Dialog Service available.'
);
return;
}
@ -27,13 +28,12 @@ function getCommandsModule ({ servicesManager }) {
showOverlay: true,
content: Test,
});
},
storeContexts: [],
options: {},
context: 'VIEWER',
},
}
};
return {
definitions,
@ -47,19 +47,22 @@ function Test() {
dispatch,
] = useViewportGrid();
return <div
onClick={() => {
dispatch({ type: '', payload: {
numCols: 2,
numRows: 2,
activeViewportIndex: 0,
viewports: [],
}})
}}
style={{ color: 'white' }}
>
Hello World!
</div>
return (
<div
onClick={() => {
dispatch({
type: 'SET_LAYOUT',
payload: {
numCols: 2,
numRows: 2,
},
});
}}
style={{ color: 'white' }}
>
Hello World!
</div>
);
}
export default getCommandsModule;

View File

@ -1,7 +1,7 @@
import React from 'react';
import { WrappedPanelStudyBrowser } from './Panels';
import MeasurementTable from './MeasurementTable.js';
import PanelMeasurementTable from './PanelMeasurementTable.js';
// TODO:
// - No loading UI exists yet
@ -15,7 +15,7 @@ function getPanelModule({
}) {
const wrappedMeasurementPanel = () => {
return (
<MeasurementTable
<PanelMeasurementTable
commandsManager={commandsManager}
servicesManager={servicesManager}
/>

View File

@ -1,8 +0,0 @@
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 });
};

View File

@ -1,38 +0,0 @@
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: 'OHIFExtDicomP10Downloader',
libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main,
},
});
};

View File

@ -1,28 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.1.0](https://github.com/OHIF/Viewers/compare/@ohif/extension-dicom-p10-downloader@0.0.2...@ohif/extension-dicom-p10-downloader@0.1.0) (2020-04-23)
### Features
* configuration to hook into XHR Error handling ([e96205d](https://github.com/OHIF/Viewers/commit/e96205de35e5bec14dc8a9a8509db3dd4e6ecdb6))
## 0.0.2 (2020-04-15)
**Note:** Version bump only for package @ohif/extension-dicom-p10-downloader
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.

View File

@ -1,21 +0,0 @@
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.

View File

@ -1 +0,0 @@
# @ohif/extension-dicom-p10-downloader

View File

@ -1,40 +0,0 @@
{
"name": "@ohif/extension-dicom-p10-downloader",
"version": "0.1.0",
"description": "OHIF extension for downloading DICOM P10 files",
"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",
"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": "^2.6.0",
"dicom-parser": "^1.8.3",
"dicomweb-client": "^0.5.2"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"dicomweb-client": "^0.6.0",
"file-saver": "^2.0.2",
"jszip": "^3.2.2"
}
}

View File

@ -1,103 +0,0 @@
import OHIF from '@ohif/core';
import {
save,
getDicomWebClientFromContext,
getSOPInstanceReferenceFromActiveViewport,
getSOPInstanceReferencesFromViewports,
} from './utils';
import _downloadAndZip from './downloadAndZip';
const {
utils: { Queue },
} = OHIF;
export function getCommands(context) {
const queue = new Queue(1);
const actions = {
/**
* @example Running this command using Commands Manager
* commandsManager.runCommand(
* 'downloadAndZip',
* {
* listOfUIDs: [...],
* options: {
* progress(status) {
* console.info('Progress:', (status.progress * 100).toFixed(2) + '%');
* }
* }
* },
* 'VIEWER'
* );
*/
downloadAndZip({ servers, dicomWebClient, listOfUIDs, options }) {
return save(
_downloadAndZip(
dicomWebClient || getDicomWebClientFromContext(context, servers),
listOfUIDs,
options
),
listOfUIDs
);
},
downloadAndZipSeriesOnViewports({ servers, viewports, progress }) {
const dicomWebClient = getDicomWebClientFromContext(context, servers);
const listOfUIDs = getSOPInstanceReferencesFromViewports(viewports);
return save(
_downloadAndZip(dicomWebClient, listOfUIDs, { progress }),
listOfUIDs
);
},
downloadAndZipSeriesOnActiveViewport({ servers, viewports, progress }) {
const dicomWebClient = getDicomWebClientFromContext(context, servers);
const listOfUIDs = getSOPInstanceReferenceFromActiveViewport(viewports);
return save(
_downloadAndZip(dicomWebClient, listOfUIDs, { progress }),
listOfUIDs
);
},
};
const definitions = {
downloadAndZip: {
commandFn: queue.bindSafe(actions.downloadAndZip, error),
storeContexts: ['servers'],
},
downloadAndZipSeriesOnViewports: {
commandFn: queue.bindSafe(actions.downloadAndZipSeriesOnViewports, error),
storeContexts: ['servers', 'viewports'],
options: { progress },
},
downloadAndZipSeriesOnActiveViewport: {
commandFn: queue.bindSafe(
actions.downloadAndZipSeriesOnActiveViewport,
error
),
storeContexts: ['servers', 'viewports'],
options: { progress },
},
};
return {
actions,
definitions,
};
}
/**
* Utils
*/
function progress(status) {
OHIF.log.info(
'Download and Zip Progress:',
(status.progress * 100.0).toFixed(2) + '%'
);
}
function error(e) {
if (e.message === 'Queue limit reached') {
OHIF.log.warn('A download is already in progress, please wait.');
} else {
OHIF.log.error(e);
}
}

View File

@ -1,244 +0,0 @@
import OHIF from '@ohif/core';
import { api } from 'dicomweb-client';
import dicomParser from 'dicom-parser';
import JSZip from 'jszip';
/**
* Constants
*/
const {
utils: {
isDicomUid,
hierarchicalListUtils,
progressTrackingUtils: progressUtils,
},
} = OHIF;
/**
* Public Methods
*/
/**
* Download and Zip all DICOM P10 instances from specified DICOM Web Client
* based on an hierarchical list of UIDs;
*
* @param {DICOMwebClient} dicomWebClient A DICOMwebClient instance through
* which the referenced instances will be retrieved;
* @param {Array} listOfUIDs The hierarchical list of UIDs from the instances
* that should be retrieved:
* A hierarchical list of UIDs is a regular JS Array where the type of the UID
* (study, series, instance) is determined by its nasting lavel. For example:
* @ The following list instructs the library to download all the instances
* from both studies "A" and "B":
*
* ['studyUIDFromA', 'studyUIDFromB']
*
* @ In the previous example both UIDs are treated as STUDY UIDs because both
* of them are listed in the same (top) level of the list. If, on the other
* hand, only instances from series "I" and "J" from the study "B"
* are to be downloaded, the expected hierarchical list would be:
*
* ['studyUIDFromA', ['studyUIDFromB', ['seriesUIDFromI', 'seriesUIDFromJ']]]
*
* @ Which, when prettified, reads like this:
*
* [
* 'studyUIDFromA',
* ['studyUIDFromB', [
* 'seriesUIDFromI',
* 'seriesUIDFromJ'
* ]]
* ]
*
* @ Furthermore, if only instances "X", "Y" and "Z" from series "J" need to
* be downloaded (instead of all the instances from that series), the list
* could be changed to:
*
* [
* 'studyUIDFromA',
* ['studyUIDFromB', [
* 'seriesUIDFromI',
* ['seriesUIDFromJ', [
* 'instanceUIDFromX',
* 'instanceUIDFromY',
* 'instanceUIDFromZ'
* ]]
* ]]
* ]
*
* Please refer to hierarchicalListUtils.js for more information and utilities;
*
* @param {Object} options A plain object with options;
* @param {function} options.progress A callback to retrieve notifications
* @returns {Promise} A promise that resolves to an URL from which the ZIP file
* can be downloaded;
*/
async function downloadAndZip(dicomWebClient, listOfUIDs, options) {
if (dicomWebClient instanceof api.DICOMwebClient) {
const settings = buildSettings(listOfUIDs, options);
const { compression } = settings.tasks;
// Register user-provided progress handler as a task list observer
progressUtils.addObserver(settings.taskList, settings.options.progress);
const buffers = await downloadAll(dicomWebClient, settings).catch(error => {
// Reject promise from compression task on download failure
compression.deferred.reject(error);
throw error;
});
compression.deferred.resolve(zipAll(buffers, settings));
const url = await compression.deferred.promise;
return url;
}
throw new Error('A valid DICOM Web Client instance is expected');
}
/**
* Utils
*/
async function zipAll(buffers, settings) {
const zip = new JSZip();
OHIF.log.info('Adding DICOM P10 files to archive:', buffers.length);
buffers.forEach((buffer, i) => {
const path = buildPath(buffer) || `${i}.dcm`;
zip.file(path, buffer);
});
// Set compression task progress to 50%
progressUtils.update(settings.tasks.compression.task, 0.5);
const blob = await zip.generateAsync({ type: 'blob' });
return URL.createObjectURL(blob);
}
function buildSettings(listOfUIDs, options) {
const taskList = progressUtils.createList();
const compression = progressUtils.addDeferred(taskList);
const downloads = [];
// Build downloads list
hierarchicalListUtils.forEach(
listOfUIDs,
(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) => {
if (isDicomUid(StudyInstanceUID)) {
downloads.push({
tracking: progressUtils.addDeferred(taskList),
parameters: [StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID],
});
}
}
);
// Print tree of hierarchical references
OHIF.log.info('Downloading DICOM P10 files for references:');
OHIF.log.info(hierarchicalListUtils.print(listOfUIDs));
return {
options: Object(options),
taskList,
tasks: {
downloads,
compression,
},
};
}
function buildPath(buffer) {
let path;
try {
const byteArray = new Uint8Array(buffer);
const dataSet = dicomParser.parseDicom(byteArray, {
// Stop parsing after SeriesInstanceUID is found
untilTag: 'x0020000e',
});
const StudyInstanceUID = dataSet.string('x0020000d');
const SeriesInstanceUID = dataSet.string('x0020000e');
const SOPInstanceUID = dataSet.string('x00080018');
if (StudyInstanceUID && SeriesInstanceUID && SOPInstanceUID) {
path = `${StudyInstanceUID}/${SeriesInstanceUID}/${SOPInstanceUID}.dcm`;
}
} catch (e) {
OHIF.log.error('Error parsing downloaded DICOM P10 file...', e);
}
return path;
}
async function downloadAll(dicomWebClient, settings) {
const { downloads } = settings.tasks;
// Make sure at least one download was initiated
if (downloads.length < 1) {
throw new Error('No valid reference to be downloaded');
}
const promises = downloads.map(item => {
const {
parameters,
tracking: { deferred, task },
} = item;
deferred.resolve(download(task, dicomWebClient, ...parameters));
return deferred.promise;
});
// Wait on created download promises
return Promise.all(promises).then(results => {
const buffers = [];
// The "results" array may directly contain buffers (ArrayBuffer instances)
// or arrays of buffers, depending on the type of downloads initiated on the
// previous step (retrieveStudy, retrieveSeries or retrieveinstance). Ex:
// results = [buf1, [buf2, buf3], buf4, [buf5], ...];
results.forEach(
function select(nesting, result) {
if (result instanceof ArrayBuffer) {
buffers.push(result);
} else if (nesting && Array.isArray(result)) {
// "nesting" argument is important to make sure only two levels
// of arrays are visited. For example, "bufX" should not be visited:
// [buf1, [buf2, buf3, [bufX]], buf4, [buf5], ...];
result.forEach(select.bind(null, false));
}
}.bind(null, true)
);
return buffers;
});
}
async function download(
task,
dicomWebClient,
studyInstanceUID,
seriesInstanceUID,
sopInstanceUID
) {
// Strict DICOM-formatted variable names COULDN'T be used here because the
// DICOM Web client interface expects them in this specific format.
// @TODO: Add support for download progress handler which will use the
// currently not use "task" param
if (!isDicomUid(studyInstanceUID)) {
throw new Error('Download requires at least a "StudyInstanceUID" property');
}
if (!isDicomUid(seriesInstanceUID)) {
// Download entire study
return dicomWebClient.retrieveStudy({
studyInstanceUID,
});
}
if (!isDicomUid(sopInstanceUID)) {
// Download entire series
return dicomWebClient.retrieveSeries({
studyInstanceUID,
seriesInstanceUID,
});
}
// Download specific instance
return dicomWebClient.retrieveInstance({
studyInstanceUID,
seriesInstanceUID,
sopInstanceUID,
});
}
/**
* Exports
*/
export { downloadAndZip as default, downloadAndZip };

View File

@ -1,43 +0,0 @@
import { getDicomWebClientFromConfig } from './utils';
import { getCommands } from './commandsModule';
/**
* Constants
*/
/**
* Globals
*/
const sharedContext = {
dicomWebClient: null,
};
/**
* Extension
*/
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'dicom-p10-downloader',
/**
* LIFECYCLE HOOKS
*/
preRegistration({ appConfig }) {
const dicomWebClient = getDicomWebClientFromConfig(appConfig);
if (dicomWebClient) {
sharedContext.dicomWebClient = dicomWebClient;
}
},
/**
* MODULE GETTERS
*/
getCommandsModule() {
return getCommands(sharedContext);
},
};

View File

@ -1,110 +0,0 @@
import OHIF from '@ohif/core';
import { api } from 'dicomweb-client';
import { saveAs } from 'file-saver';
const {
utils: { isDicomUid, resolveObjectPath, hierarchicalListUtils },
DICOMWeb,
} = OHIF;
function validDicomUid(subject) {
if (isDicomUid(subject)) {
return subject;
}
}
function getActiveServerFromServersStore(store) {
const servers = resolveObjectPath(store, 'servers');
if (Array.isArray(servers) && servers.length > 0) {
return servers.find(server => resolveObjectPath(server, 'active') === true);
}
}
function getDicomWebClientFromConfig(config) {
const servers = resolveObjectPath(config, 'servers.dicomWeb');
if (Array.isArray(servers) && servers.length > 0) {
const server = servers[0];
return new api.DICOMwebClient({
url: server.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(server),
});
}
}
function getDicomWebClientFromContext(context, store) {
const activeServer = getActiveServerFromServersStore(store);
if (activeServer) {
return new api.DICOMwebClient({
url: activeServer.wadoRoot,
headers: DICOMWeb.getAuthorizationHeader(activeServer),
});
} else if (context.dicomWebClient instanceof api.DICOMwebClient) {
return context.dicomWebClient;
}
}
function getSOPInstanceReference(viewports, index) {
if (index >= 0) {
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = Object(
resolveObjectPath(viewports, `viewportSpecificData.${index}`)
);
return Object.freeze(
hierarchicalListUtils.addToList(
[],
validDicomUid(StudyInstanceUID),
validDicomUid(SeriesInstanceUID),
validDicomUid(SOPInstanceUID)
)
);
}
}
function getSOPInstanceReferenceFromActiveViewport(viewports) {
return getSOPInstanceReference(
viewports,
resolveObjectPath(viewports, 'activeViewportIndex')
);
}
function getSOPInstanceReferencesFromViewports(viewports) {
const list = [];
const viewportSpecificData = resolveObjectPath(
viewports,
'viewportSpecificData'
);
Object.keys(viewportSpecificData).forEach(index => {
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = Object(
viewportSpecificData[index]
);
hierarchicalListUtils.addToList(
list,
validDicomUid(StudyInstanceUID),
validDicomUid(SeriesInstanceUID),
validDicomUid(SOPInstanceUID)
);
});
return list;
}
function save(promise, listOfUIDs) {
return Promise.resolve(promise)
.then(url => {
OHIF.log.info('Files successfully compressed:', url);
const StudyInstanceUID = hierarchicalListUtils.getItem(listOfUIDs, 0);
saveAs(url, `${StudyInstanceUID}.zip`);
return url;
})
.catch(error => {
OHIF.log.error('Failed to create Zip file...', error);
return null;
});
}
export {
save,
validDicomUid,
getDicomWebClientFromConfig,
getDicomWebClientFromContext,
getSOPInstanceReferenceFromActiveViewport,
getSOPInstanceReferencesFromViewports,
};

View File

@ -1,8 +0,0 @@
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 });
};

View File

@ -1,38 +0,0 @@
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,
},
});
};

View File

@ -1,22 +0,0 @@
# 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))

View File

@ -1,21 +0,0 @@
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.

View File

@ -1,38 +0,0 @@
# @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 -->

View File

@ -1,39 +0,0 @@
{
"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",
"start": "yarn run dev"
},
"peerDependencies": {
"@ohif/core": "^0.50.0",
"prop-types": "^15.6.2",
"react": "^16.11.0",
"react-dom": "^16.11.0"
},
"dependencies": {
"@babel/runtime": "7.7.6",
"classnames": "^2.2.6"
}
}

View File

@ -1,60 +0,0 @@
.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);
}

View File

@ -1,151 +0,0 @@
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;

View File

@ -1,52 +0,0 @@
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;
},
};

View File

@ -0,0 +1,88 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { Machine } from 'xstate';
import { useMachine } from '@xstate/react';
import {
machineConfiguration,
defaultOptions,
} from './measurementTrackingMachine';
const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext);
/**
*
* @param {*} param0
*/
function TrackedMeasurementsContextProvider(
UIViewportDialogService,
{ children }
) {
function promptUser(message, ctx, evt) {
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
return new Promise(function(resolve, reject) {
/**
* TODO: Will have issues if "SeriesInstanceUID" exists in multiple displaySets?
*
* @param {number} result - -1 | 0 | 1 --> deny | cancel | accept
* @return resolve { userResponse: number, StudyInstanceUID: string, SeriesInstanceUID: string }
*/
const handleSubmit = result => {
UIViewportDialogService.hide();
resolve({ userResponse: result, StudyInstanceUID, SeriesInstanceUID });
};
UIViewportDialogService.show({
viewportIndex,
type: 'info',
message,
actions: [
{ type: 'cancel', text: 'No', value: 0 },
{ type: 'secondary', text: 'No, do not ask again', value: -1 },
{ type: 'primary', text: 'Yes', value: 1 },
],
onSubmit: handleSubmit,
});
});
}
// Set StateMachine behavior for prompts (invoked services)
const machineOptions = Object.assign({}, defaultOptions);
machineOptions.services = Object.assign({}, machineOptions.services, {
promptBeginTracking: promptUser.bind(null, 'Start tracking?'),
promptTrackNewStudy: promptUser.bind(null, 'New study?'),
promptTrackNewSeries: promptUser.bind(null, 'New series?'),
});
const measurementTrackingMachine = Machine(
machineConfiguration,
machineOptions
);
const [
trackedMeasurements,
sendTrackedMeasurementsEvent,
trackedMeasurementsService,
] = useMachine(measurementTrackingMachine);
return (
<TrackedMeasurementsContext.Provider
value={[trackedMeasurements, sendTrackedMeasurementsEvent]}
>
{children}
</TrackedMeasurementsContext.Provider>
);
}
TrackedMeasurementsContextProvider.propTypes = {
children: PropTypes.oneOf([PropTypes.func, PropTypes.node]),
};
export {
TrackedMeasurementsContext,
TrackedMeasurementsContextProvider,
useTrackedMeasurements,
};

View File

@ -1,45 +1,5 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { useMachine } from '@xstate/react';
import {
measurementTrackingMachine,
defaultOptions,
} from './measurementTrackingMachine';
const TrackedMeasurementsContext = React.createContext();
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
const useTrackedMeasurements = () => useContext(TrackedMeasurementsContext);
/**
*
* @param {*} param0
*/
function TrackedMeasurementsContextProvider({ children }) {
// TODO: Configure `UIViewportNotificationService` for appropriate actions
const measurementTrackingMachineOptions = Object.assign({}, defaultOptions);
const [
trackedMeasurements,
sendTrackedMeasurementsEvent,
trackedMeasurementsService,
] = useMachine(measurementTrackingMachine, measurementTrackingMachineOptions);
return (
<TrackedMeasurementsContext.Provider
value={[trackedMeasurements, sendTrackedMeasurementsEvent]}
>
{children}
</TrackedMeasurementsContext.Provider>
);
}
TrackedMeasurementsContextProvider.propTypes = {
children: PropTypes.oneOf([PropTypes.func, PropTypes.node]),
};
export {
TrackedMeasurementsContext,
TrackedMeasurementsContextProvider,
useTrackedMeasurements,
};
} from './TrackedMeasurementsContext.jsx';

View File

@ -1,124 +1,101 @@
import { Machine, assign } from 'xstate';
// import { assign } from '@xstate/immer';
import { assign } from 'xstate';
const machineConfiguration = {
id: 'measurementTracking',
initial: 'notTracking',
initial: 'idle',
context: {
prevTrackedStudy: '',
prevTrackedSeries: [],
trackedStudy: '',
trackedSeries: [],
promptResponse: 0,
},
states: {
notTracking: {
off: {
type: 'final',
},
idle: {
entry: 'clearContext',
on: {
TRACK_SERIES: {
target: 'awaitShouldTrackPrompt',
actions: 'trackSeries',
},
TRACK_SERIES: 'promptBeginTracking',
},
},
awaitShouldTrackPrompt: {
initial: 'prompt',
states: {
prompt: {
invoke: {
id: 'shouldTrackPrompt',
src: () => confirmDialog('Should we start tracking?'),
onDone: {
target: 'validateResponse',
actions: assign({ promptResponse: (ctx, evt) => evt.data }),
},
onError: {
target: 'validateResponse',
actions: assign({ promptResponse: (ctx, evt) => evt.data }),
},
promptBeginTracking: {
invoke: {
src: 'promptBeginTracking',
onDone: [
{
target: 'tracking',
actions: ['setTrackedStudyAndSeries'],
cond: 'promptAccepted',
},
},
validateResponse: {
on: {
'': [
{
target: '#measurementTracking.tracking',
cond: 'acceptResponse',
},
{
target: '#measurementTracking.neverTrack',
cond: 'rejectResponse',
},
{
target: '#measurementTracking.notTracking',
cond: 'cancelResponse',
},
],
{
target: 'off',
cond: 'promptDeclined',
},
{
target: 'idle',
},
],
onError: {
target: 'idle',
},
},
},
neverTrack: {
type: 'final',
},
tracking: {
on: {
TRACK_SERIES: [
{
target: 'tracking',
cond: 'hasNewSeriesForTrackedStudy',
actions: 'trackSeries',
target: 'promptTrackNewStudy',
cond: 'isNewStudy',
},
{
target: 'awaitShouldTrackNewStudyPrompt',
cond: 'willTrackNewStudy',
actions: ['setPrevTracked', 'clearTrackedSeries', 'trackSeries'],
target: 'promptTrackNewSeries',
cond: 'isNewSeries',
},
],
UNTRACK_SERIES: [
{
target: 'notTracking',
cond: 'willBeEmpty',
actions: 'untrackSeries',
target: 'tracking',
actions: ['removeTrackedSeries'],
cond: 'hasRemainingTrackedSeries',
},
{
target: 'tracking',
actions: 'untrackSeries',
target: 'idle',
},
],
},
},
awaitShouldTrackNewStudyPrompt: {
initial: 'prompt',
states: {
prompt: {
invoke: {
id: 'shouldTrackPrompt',
src: () => confirmDialog('Should we track new study?'),
onDone: {
target: 'validateResponse',
actions: assign({ promptResponse: (ctx, evt) => evt.data }),
},
onError: {
target: 'validateResponse',
actions: assign({ promptResponse: (ctx, evt) => evt.data }),
},
promptTrackNewStudy: {
invoke: {
src: 'promptTrackNewStudy',
onDone: [
{
target: 'tracking',
actions: ['setTrackedStudyAndSeries'],
cond: 'promptAccepted',
},
{
target: 'tracking',
},
],
onError: {
target: 'idle',
},
validateResponse: {
on: {
'': [
{
target: '#measurementTracking.tracking',
cond: 'acceptResponse',
},
{
target: '#measurementTracking.tracking',
cond: 'rejectResponse',
actions: 'restorePrevTracked',
},
],
},
},
promptTrackNewSeries: {
invoke: {
src: 'promptTrackNewSeries',
onDone: [
{
target: 'tracking',
actions: ['addTrackedSeries'],
cond: 'promptAccepted',
},
{
target: 'tracking',
},
],
onError: {
target: 'idle',
},
},
},
@ -126,71 +103,59 @@ const machineConfiguration = {
strict: true,
};
function confirmDialog(msg) {
return new Promise(function(resolve, reject) {
let confirmed = window.confirm(msg);
return confirmed ? resolve(1) : reject(-1);
});
}
const defaultOptions = {
services: {
promptBeginTracking: (ctx, evt) => {
// return { userResponse, StudyInstanceUID, SeriesInstanceUID }
},
promptTrackNewStudy: (ctx, evt) => {
// return { userResponse, StudyInstanceUID, SeriesInstanceUID }
},
promptTrackNewSeries: (ctx, evt) => {
// return { userResponse, StudyInstanceUID, SeriesInstanceUID }
},
},
actions: {
clearContext: assign({
prevTrackedStudy: '',
prevTrackedSeries: [],
trackedStudy: '',
trackedSeries: [],
promptResponse: 0,
}),
setPrevTracked: assign(ctx => ({
prevTrackedStudy: ctx.trackedStudy,
prevTrackedSeries: ctx.trackedSeries.slice(),
// Promise resolves w/ `evt.data.*`
setTrackedStudyAndSeries: assign((ctx, evt) => ({
trackedStudy: evt.data.StudyInstanceUID,
trackedSeries: [evt.data.SeriesInstanceUID],
})),
restorePrevTracked: assign(ctx => ({
trackedStudy: ctx.prevTrackedStudy,
trackedSeries: ctx.prevTrackedSeries.slice(),
addTrackedSeries: assign((ctx, evt) => ({
trackedSeries: [...ctx.trackedSeries, evt.data.SeriesInstanceUID],
})),
clearTrackedSeries: assign(() => ({
trackedStudy: '',
trackedSeries: [],
removeTrackedSeries: assign((ctx, evt) => ({
trackedSeries: ctx.trackedSeries
.slice()
.filter(ser => ser !== evt.SeriesInstanceUID),
})),
trackSeries: assign((ctx, evt) => {
const prevTrackedSeries = ctx.trackedSeries.slice();
return {
trackedStudy: evt.StudyInstanceUID || '',
trackedSeries: [...prevTrackedSeries, evt.SeriesInstanceUID],
};
}),
untrackSeries: assign((ctx, evt) => {
const prevTrackedSeries = ctx.trackedSeries.slice();
return {
trackedSeries: prevTrackedSeries.filter(
serUid => serUid !== evt.SeriesInstanceUID
),
};
}),
},
guards: {
hasNewSeriesForTrackedStudy: (ctx, evt) =>
evt.StudyInstanceUID === ctx.trackedStudy &&
promptAccepted: (ctx, evt) => evt.data && evt.data.userResponse === 1,
promptCanceled: (ctx, evt) => evt.data && evt.data.userResponse === 0,
promptDeclined: (ctx, evt) => evt.data && evt.data.userResponse === -1,
// Has more than 1, or SeriesInstanceUID is not in list
// --> Post removal would have non-empty trackedSeries array
hasRemainingTrackedSeries: (ctx, evt) =>
ctx.trackedSeries.length > 1 ||
!ctx.trackedSeries.includes(evt.SeriesInstanceUID),
isNewStudy: (ctx, evt) => ctx.trackedStudy !== evt.StudyInstanceUID,
isNewSeries: (ctx, evt) =>
!ctx.trackedSeries.includes(evt.SeriesInstanceUID),
willTrackNewStudy: ctx => ctx.StudyInstanceUID !== ctx.trackedStudy,
willBeEmpty: (ctx, evt) =>
evt.SeriesInstanceUID &&
ctx.trackedSeries.length === 1 &&
ctx.trackedSeries[0] === evt.SeriesInstanceUID,
acceptResponse: ctx => ctx.promptResponse === 1,
cancelResponse: ctx => ctx.promptResponse === 0,
rejectResponse: ctx => ctx.promptResponse === -1,
},
};
const measurementTrackingMachine = Machine(machineConfiguration, defaultOptions);
// const measurementTrackingMachine = Machine(
// machineConfiguration,
// defaultOptions
// );
// .transition(state, eventArgument).value
// const service = interpret(measurementTrackingMachine).start();
// .send(event): nextState
// .state (getter)
// .onTransition(state => { state.vale })
export { defaultOptions, machineConfiguration, measurementTrackingMachine };
export default measurementTrackingMachine;
export { defaultOptions, machineConfiguration };

View File

@ -10,7 +10,9 @@ function getCommandsModule({ servicesManager }) {
toggleLayoutSelectionDialog: {
commandFn: () => {
if (!UIDialogService) {
window.alert('Unable to show dialog; no UI Dialog Service available.');
window.alert(
'Unable to show dialog; no UI Dialog Service available.'
);
return;
}
@ -26,13 +28,12 @@ function getCommandsModule({ servicesManager }) {
showOverlay: true,
content: Test,
});
},
storeContexts: [],
options: {},
context: 'VIEWER',
},
}
};
return {
definitions,
@ -46,19 +47,22 @@ function Test() {
dispatch,
] = useViewportGrid();
return <div
onClick={() => {
dispatch({ type: '', payload: {
numCols: 2,
numRows: 2,
activeViewportIndex: 0,
viewports: [],
}})
}}
style={{ color: 'white' }}
>
Hello World!
</div>
return (
<div
onClick={() => {
dispatch({
type: 'SET_LAYOUT',
payload: {
numCols: 2,
numRows: 2,
},
});
}}
style={{ color: 'white' }}
>
Hello World!
</div>
);
}
export default getCommandsModule;

View File

@ -4,12 +4,18 @@ import {
useTrackedMeasurements,
} from './contexts';
function getContextModule() {
function getContextModule({ servicesManager }) {
const { UIViewportDialogService } = servicesManager.services;
const BoundTrackedMeasurementsContextProvider = TrackedMeasurementsContextProvider.bind(
null,
UIViewportDialogService
);
return [
{
name: 'TrackedMeasurementsContext',
context: TrackedMeasurementsContext,
provider: TrackedMeasurementsContextProvider,
provider: BoundTrackedMeasurementsContextProvider,
},
];
}

View File

@ -25,7 +25,7 @@ function getPanelModule({
}),
},
{
name: 'measure',
name: 'trackedMeasurements',
iconName: 'list-bullets',
iconLabel: 'Measure',
label: 'Measurements',

View File

@ -1,7 +1,7 @@
import React from 'react';
const Component = React.lazy(() => {
return import('./viewports/OHIFCornerstoneViewport');
return import('./viewports/TrackedCornerstoneViewport');
});
const OHIFCornerstoneViewport = props => {

View File

@ -0,0 +1,37 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonGroup, Icon, IconButton } from '@ohif/ui';
function ActionButtons() {
return (
<React.Fragment>
<ButtonGroup onClick={() => alert('Export')}>
<Button
className="px-2 py-2 text-base text-white bg-black border-primary-main"
size="initial"
color="inherit"
>
Export
</Button>
<IconButton
className="px-2 text-white bg-black border-primary-main"
color="inherit"
size="initial"
>
<Icon name="arrow-down" />
</IconButton>
</ButtonGroup>
<Button
className="px-2 py-2 ml-2 text-base text-white bg-black border border-primary-main"
variant="outlined"
size="initial"
color="inherit"
onClick={() => alert('Create Report')}
>
Create Report
</Button>
</React.Fragment>
);
}
export default ActionButtons;

View File

@ -1,74 +1,202 @@
import React from 'react';
import {
MeasurementsPanel,
Button,
ButtonGroup,
Icon,
IconButton,
} from '@ohif/ui';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { StudySummary, MeasurementTable } from '@ohif/ui';
import { DicomMetadataStore } from '@ohif/core';
import { useDebounce } from '@hooks';
import ActionButtons from './ActionButtons';
import { useTrackedMeasurements } from '../../getContextModule';
export default function MeasurementTable({ servicesManager, commandsManager }) {
const { MeasurementService } = servicesManager.services;
const DISPLAY_STUDY_SUMMARY_INITIAL_VALUE = {
key: undefined, //
date: undefined, // '07-Sep-2010',
modality: undefined, // 'CT',
description: undefined, // 'CHEST/ABD/PELVIS W CONTRAST',
};
console.log('MeasurementTable rendering!!!!!!!!!!!!!');
const actionButtons = (
<React.Fragment>
<ButtonGroup onClick={() => alert('Export')}>
<Button
className="px-2 py-2 text-base text-white bg-black border-primary-main"
size="initial"
color="inherit"
>
Export
</Button>
<IconButton
className="px-2 text-white bg-black border-primary-main"
color="inherit"
size="initial"
>
<Icon name="arrow-down" />
</IconButton>
</ButtonGroup>
<Button
className="px-2 py-2 ml-2 text-base text-white bg-black border border-primary-main"
variant="outlined"
size="initial"
color="inherit"
onClick={() => alert('Create Report')}
>
Create Report
</Button>
</React.Fragment>
function PanelMeasurementTableTracking({ servicesManager, commandsManager }) {
const [measurementChangeTimestamp, setMeasurementsUpdated] = useState(
Date.now().toString()
);
const debouncedMeasurementChangeTimestamp = useDebounce(
measurementChangeTimestamp,
200
);
const { MeasurementService } = servicesManager.services;
const [
trackedMeasurements,
sendTrackedMeasurementsEvent,
] = useTrackedMeasurements();
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
const [displayStudySummary, setDisplayStudySummary] = useState(
DISPLAY_STUDY_SUMMARY_INITIAL_VALUE
);
const [displayMeasurements, setDisplayMeasurements] = useState([]);
// TODO: measurements subscribtion
const descriptionData = {
date: '07-Sep-2010',
modality: 'CT',
description: 'CHEST/ABD/PELVIS W CONTRAST',
};
// Initial?
useEffect(() => {
const measurements = MeasurementService.getMeasurements();
const filteredMeasurements = measurements.filter(
m =>
trackedStudy === m.referenceStudyUID &&
trackedSeries.includes(m.referenceSeriesUID)
);
const mappedMeasurements = filteredMeasurements.map((m, index) =>
_mapMeasurementToDisplay(m, index)
);
setDisplayMeasurements(mappedMeasurements);
// eslint-ignore-next-line
}, [
MeasurementService,
trackedStudy,
trackedSeries,
debouncedMeasurementChangeTimestamp,
]);
// ~~ DisplayStudySummary
useEffect(() => {
if (trackedMeasurements.matches('tracking')) {
const StudyInstanceUID = trackedStudy;
const studyMeta = DicomMetadataStore.getStudy(StudyInstanceUID);
const instanceMeta = studyMeta.series[0].instances[0];
const { Modality, StudyDate, StudyDescription } = instanceMeta;
if (displayStudySummary.key !== StudyInstanceUID) {
setDisplayStudySummary({
key: StudyInstanceUID,
date: StudyDate, // TODO: Format: '07-Sep-2010'
modality: Modality,
description: StudyDescription,
});
}
} else if (trackedStudy === '' || trackedStudy === undefined) {
setDisplayStudySummary(DISPLAY_STUDY_SUMMARY_INITIAL_VALUE);
}
}, [displayStudySummary.key, trackedMeasurements, trackedStudy]);
// TODO: Better way to consolidated, debounce, check on change?
// Are we exposing the right API for measurementService?
// This watches for ALL MeasurementService changes. It updates a timestamp,
// which is debounced. After a brief period of inactivity, this triggers
// a re-render where we grab up-to-date measurements.
useEffect(() => {
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
const subscriptions = [];
[added, updated, removed].forEach(evt => {
subscriptions.push(
MeasurementService.subscribe(evt, () => {
setMeasurementsUpdated(Date.now().toString());
})
);
});
return () => {
subscriptions.forEach(unsub => {
unsub();
});
};
}, [MeasurementService, sendTrackedMeasurementsEvent]);
const activeMeasurementItem = 0;
const measurementTableData = {
title: 'Measurements',
amount: 10,
data: new Array(10).fill({}).map((el, i) => ({
id: i + 1,
label: 'Label short description',
displayText: '24.0 x 24.0 mm (S:4, I:22)',
isActive: activeMeasurementItem === i + 1,
})),
// onClick: id => setActiveMeasurementItem(s => (s === id ? null : id)),
onClick: () => {},
onEdit: id => alert(`Edit: ${id}`),
};
return (
<MeasurementsPanel
descriptionData={descriptionData}
measurementTableData={measurementTableData}
actionButtons={actionButtons}
/>
<>
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
{displayStudySummary.key && (
<StudySummary
date={displayStudySummary.date}
modality={displayStudySummary.modality}
description={displayStudySummary.description}
/>
)}
<MeasurementTable
title="Measurements"
amount={displayMeasurements.length}
data={displayMeasurements}
onClick={() => {}}
onEdit={id => alert(`Edit: ${id}`)}
/>
</div>
<div className="flex justify-center p-4">
<ActionButtons />
</div>
</>
);
}
PanelMeasurementTableTracking.propTypes = {};
// TODO: This could be a MeasurementService mapper
function _mapMeasurementToDisplay(measurement, index) {
const {
id,
label,
description,
// Reference IDs
referenceStudyUID,
referenceSeriesUID,
SOPInstanceUID,
} = measurement;
const instance = DicomMetadataStore.getInstance(
referenceStudyUID,
referenceSeriesUID,
SOPInstanceUID
);
const { PixelSpacing, SeriesNumber, InstanceNumber } = instance;
console.log('mapping....', measurement);
console.log(instance);
return {
id: index + 1,
label: '(empty)', // 'Label short description',
displayText: _getDisplayText(
measurement.points,
PixelSpacing,
SeriesNumber,
InstanceNumber
),
// TODO: handle one layer down
isActive: false, // activeMeasurementItem === i + 1,
};
}
/**
*
* @param {*} points
* @param {*} pixelSpacing
*/
function _getDisplayText(points, pixelSpacing, seriesNumber, instanceNumber) {
// TODO: determination of shape influences text
// Length: 'xx.x unit (S:x, I:x)'
// Rectangle: 'xx.x x xx.x unit (S:x, I:x)',
// Ellipse?
// Bidirectional?
// Freehand?
const hasPixelSpacing =
pixelSpacing !== undefined &&
Array.isArray(pixelSpacing) &&
pixelSpacing.length === 2;
const [rowPixelSpacing, colPixelSpacing] = hasPixelSpacing
? pixelSpacing
: [1, 1];
const unit = hasPixelSpacing ? 'mm' : 'px';
const { x: x1, y: y1 } = points[0];
const { x: x2, y: y2 } = points[1];
const dx = (x2 - x1) * colPixelSpacing;
const dy = (y2 - y1) * rowPixelSpacing;
const length = _round(Math.sqrt(dx * dx + dy * dy), 1);
return `${length} ${unit} (S:${seriesNumber}, I:${instanceNumber})`;
}
function _round(value, decimals) {
return Number(Math.round(value + 'e' + decimals) + 'e-' + decimals);
}
export default PanelMeasurementTableTracking;

View File

@ -19,12 +19,14 @@ function PanelStudyBrowserTracking({
// doesn't have to have such an intense shape. This works well enough for now.
// Tabs --> Studies --> DisplaySets --> Thumbnails
const [{ StudyInstanceUIDs }, dispatchImageViewer] = useImageViewer();
const [{ viewports }, dispatchViewportGrid] = useViewportGrid();
const [
{ activeViewportIndex, viewports },
dispatchViewportGrid,
] = useViewportGrid();
const [
trackedMeasurements,
sendTrackedMeasurementsEvent,
] = useTrackedMeasurements();
console.warn('trackedMeasurementsState: ', trackedMeasurements);
const [activeTabName, setActiveTabName] = useState('primary');
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState(
[]
@ -33,7 +35,7 @@ function PanelStudyBrowserTracking({
const [displaySets, setDisplaySets] = useState([]);
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
// TODO: Listen for measurement service "adds" (really shouldn't be added until cornerstone-tools "complete")
// TODO: Should this be somewhere else? Feels more like a mode "lifecycle" setup/destroy?
useEffect(() => {
const { unsubscribe } = MeasurementService.subscribe(
MeasurementService.EVENTS.MEASUREMENT_ADDED,
@ -44,17 +46,15 @@ function PanelStudyBrowserTracking({
} = measurement;
sendTrackedMeasurementsEvent('TRACK_SERIES', {
viewportIndex: activeViewportIndex,
StudyInstanceUID,
SeriesInstanceUID,
});
console.log('PANEL:', measurement);
// console.log('Mapped:', annotation);
}
);
return unsubscribe;
}, [MeasurementService, sendTrackedMeasurementsEvent]);
}, [MeasurementService, activeViewportIndex, sendTrackedMeasurementsEvent]);
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
@ -156,7 +156,7 @@ function PanelStudyBrowserTracking({
changedDisplaySets,
thumbnailImageSrcMap,
trackedSeries,
viewports,
viewports
);
setDisplaySets(mappedDisplaySets);
@ -188,8 +188,11 @@ function PanelStudyBrowserTracking({
StudyInstanceUID
);
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
// eslint-disable-next-line prettier/prettier
? [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)]
? [
...expandedStudyInstanceUIDs.filter(
stdyUid => stdyUid !== StudyInstanceUID
),
]
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
@ -208,6 +211,16 @@ function PanelStudyBrowserTracking({
onClickTab={clickedTabName => {
setActiveTabName(clickedTabName);
}}
onClickUntrack={displaySetInstanceUID => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
// TODO: shift this somewhere else where we're centralizing this logic?
// Potentially a helper from displaySetInstanceUID to this
sendTrackedMeasurementsEvent('UNTRACK_SERIES', {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
});
}}
/>
);
}

View File

@ -3,12 +3,16 @@ import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import CornerstoneViewport from 'react-cornerstone-viewport';
import OHIF, { DicomMetadataStore } from '@ohif/core';
import { ViewportActionBar, useViewportGrid } from '@ohif/ui';
import {
Notification,
ViewportActionBar,
useViewportGrid,
useViewportDialog,
} from '@ohif/ui';
import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle';
import { useTrackedMeasurements } from './../getContextModule';
// const cine = viewportSpecificData.cine;
// isPlaying = cine.isPlaying === true;
@ -16,7 +20,7 @@ import { useTrackedMeasurements } from './../getContextModule';
const { StackManager } = OHIF.utils;
function OHIFCornerstoneViewport({
function TrackedCornerstoneViewport({
children,
dataSource,
displaySet,
@ -24,6 +28,8 @@ function OHIFCornerstoneViewport({
}) {
const [trackedMeasurements] = useTrackedMeasurements();
const [{ viewports }, dispatchViewportGrid] = useViewportGrid();
// viewportIndex, onSubmit
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
const [viewportData, setViewportData] = useState(null);
// TODO: Still needed? Better way than import `OHIF` and destructure?
// Why is this managed by `core`?
@ -98,7 +104,7 @@ function OHIFCornerstoneViewport({
// );
// TODO: This display contains the meta for all instances.
// That can't be right...
console.log('DISPLAYSET', displaySet)
// console.log('DISPLAYSET', displaySet);
// const seriesMeta = DicomMetadataStore.getSeries(this.props.displaySet.StudyInstanceUID, '');
// console.log(seriesMeta);
@ -145,22 +151,36 @@ function OHIFCornerstoneViewport({
},
}}
/>
<CornerstoneViewport
viewportIndex={viewportIndex}
imageIds={imageIds}
imageIdIndex={currentImageIdIndex}
// TODO: ViewportGrid Context?
isActive={true} // todo
isStackPrefetchEnabled={true} // todo
isPlaying={false}
frameRate={24}
/>
{childrenWithProps}
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
<div className="relative flex flex-row w-full h-full">
<CornerstoneViewport
viewportIndex={viewportIndex}
imageIds={imageIds}
imageIdIndex={currentImageIdIndex}
// TODO: ViewportGrid Context?
isActive={true} // todo
isStackPrefetchEnabled={true} // todo
isPlaying={false}
frameRate={24}
isOverlayVisible={false}
/>
<div className="absolute w-full">
{viewportDialogState.viewportIndex === viewportIndex && (
<Notification
message={viewportDialogState.message}
type={viewportDialogState.type}
actions={viewportDialogState.actions}
onSubmit={viewportDialogState.onSubmit}
/>
)}
</div>
{childrenWithProps}
</div>
</>
);
}
OHIFCornerstoneViewport.propTypes = {
TrackedCornerstoneViewport.propTypes = {
displaySet: PropTypes.object.isRequired,
viewportIndex: PropTypes.number.isRequired,
dataSource: PropTypes.object,
@ -168,7 +188,7 @@ OHIFCornerstoneViewport.propTypes = {
customProps: PropTypes.object,
};
OHIFCornerstoneViewport.defaultProps = {
TrackedCornerstoneViewport.defaultProps = {
customProps: {},
};
@ -207,6 +227,6 @@ async function _getViewportData(dataSource, displaySet) {
};
return viewportData;
};
}
export default OHIFCornerstoneViewport;
export default TrackedCornerstoneViewport;

View File

@ -1,3 +1,13 @@
const ohif = {
layout: 'org.ohif.default.layoutTemplateModule.viewerLayout',
sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack',
};
const tracked = {
measurements: 'org.ohif.measurement-tracking.panelModule.trackedMeasurements',
thumbnailList: 'org.ohif.measurement-tracking.panelModule.seriesList',
viewport: 'org.ohif.measurement-tracking.viewportModule.cornerstone-tracked',
};
export default function mode({ modeConfiguration }) {
return {
// TODO: We're using this as a route segment
@ -71,21 +81,15 @@ export default function mode({ modeConfiguration }) {
},
layoutTemplate: ({ routeProps }) => {
return {
id: 'org.ohif.default.layoutTemplateModule.viewerLayout',
id: ohif.layout,
props: {
// named slots
leftPanels: [
'org.ohif.measurement-tracking.panelModule.seriesList',
],
leftPanels: [tracked.thumbnailList],
// TODO: Should be optional, or required to pass empty array for slots?
rightPanels: [], // // ['org.ohif.default.panelModule.measure'],
rightPanels: [tracked.measurements],
viewports: [
{
namespace:
'org.ohif.measurement-tracking.viewportModule.cornerstone-tracked',
displaySetsToDisplay: [
'org.ohif.default.sopClassHandlerModule.stack',
],
namespace: tracked.viewport,
displaySetsToDisplay: [ohif.sopClassHandler],
},
],
},
@ -93,8 +97,12 @@ export default function mode({ modeConfiguration }) {
},
},
],
extensions: ['org.ohif.default', 'org.ohif.cornerstone', 'org.ohif.measurement-tracking'],
sopClassHandlers: ['org.ohif.default.sopClassHandlerModule.stack'],
extensions: [
'org.ohif.default',
'org.ohif.cornerstone',
'org.ohif.measurement-tracking',
],
sopClassHandlers: [ohif.sopClassHandler],
};
}

View File

@ -13,7 +13,7 @@
[build.environment]
# If 'production', `yarn install` does not install devDependencies
NODE_ENV = "development"
NODE_VERSION = "10.16.0"
NODE_VERSION = "12.13.0"
YARN_VERSION = "1.22.0"
RUBY_VERSION = "2.6.2"
YARN_FLAGS = "--no-ignore-optional --pure-lockfile"

View File

@ -16,14 +16,12 @@
"scripts": {
"cm": "npx git-cz",
"build": "lerna run build:viewer --stream",
"build:lt": "lerna run build:viewer:lesion-tracker --stream",
"build:ci": "lerna run build:viewer:ci --stream",
"build:ui:deploy-preview": "lerna run build:ui:deploy-preview --stream",
"build:demo": "lerna run build:viewer:demo --stream",
"build:package": "lerna run build:viewer:package --stream",
"build:package-all": "lerna run build:package --parallel --stream",
"dev": "lerna run dev:viewer --stream",
"dev:lt": "lerna run dev:viewer:lesion-tracker --stream",
"dev:project": ".scripts/dev.sh",
"dev:orthanc": "lerna run dev:orthanc --stream",
"orthanc:up": "docker-compose -f .docker/Nginx-Orthanc/docker-compose.yml up",

View File

@ -47,6 +47,7 @@ const MEASUREMENT_SCHEMA_KEYS = [
const EVENTS = {
MEASUREMENT_UPDATED: 'event::measurement_updated',
MEASUREMENT_ADDED: 'event::measurement_added',
MEASUREMENT_REMOVED: 'event::measurement_removed',
};
const VALUE_TYPES = {
@ -136,6 +137,9 @@ class MeasurementService {
source.addOrUpdate = (definition, measurement) => {
return this.addOrUpdate(source, definition, measurement);
};
source.remove = id => {
return this.remove(source, id);
};
source.getAnnotation = (definition, measurementId) => {
return this.getAnnotation(source, definition, measurementId);
};
@ -338,6 +342,16 @@ class MeasurementService {
return newMeasurement.id;
}
remove(source, id) {
if (!id || !this.measurements[id]) {
log.warn(`No id provided, or unable to find measurement by id.`);
return;
}
delete this.measurements[id];
this._broadcastChange(this.EVENTS.MEASUREMENT_REMOVED, source, id);
}
_getMappingByMeasurementSource(measurementId, definition) {
const measurement = this.getMeasurement(measurementId);
if (this._isValidSource(measurement.source)) {

View File

@ -17,7 +17,8 @@ const publicAPI = {
};
const serviceImplementation = {
_viewports: [],
_hide: () => console.warn('hide() NOT IMPLEMENTED'),
_show: () => console.warn('show() NOT IMPLEMENTED'),
};
/**
@ -25,39 +26,21 @@ const serviceImplementation = {
*
* @param {ViewportDialogProps} props { content, contentProps, viewportIndex }
*/
function _show({ content = null, contentProps = null, viewportIndex }) {
const viewportIndexImplementation =
(viewportIndex !== undefined &&
serviceImplementation._viewports[viewportIndex]) ||
{};
if (!viewportIndexImplementation._show) {
console.warn('show() NOT IMPLEMENTED');
return;
}
return viewportIndexImplementation._show({
content,
contentProps,
function _show({ viewportIndex, type, message, actions, onSubmit }) {
return serviceImplementation._show({
viewportIndex,
type,
message,
actions,
onSubmit,
});
}
/**
* Hides/dismisses the viewport dialog, if currently shown
*
* @param {*} { viewportIndex }
*/
function _hide({ viewportIndex }) {
const viewportIndexImplementation =
(viewportIndex && serviceImplementation._viewports[viewportIndex]) || {};
if (!viewportIndexImplementation._hide) {
console.warn('hide() NOT IMPLEMENTED');
return;
}
return viewportIndexImplementation._hide();
function _hide() {
return serviceImplementation._hide();
}
/**
@ -72,22 +55,12 @@ function _hide({ viewportIndex }) {
function setServiceImplementation({
hide: hideImplementation,
show: showImplementation,
viewportIndex,
}) {
if (viewportIndex !== undefined) {
const newImplementations = {};
if (hideImplementation) {
newImplementations._hide = hideImplementation;
}
if (showImplementation) {
newImplementations._show = showImplementation;
}
serviceImplementation._viewports[viewportIndex] = Object.assign(
{},
serviceImplementation._viewports[viewportIndex],
newImplementations
);
if (hideImplementation) {
serviceImplementation._hide = hideImplementation;
}
if (showImplementation) {
serviceImplementation._show = showImplementation;
}
}

View File

@ -41,7 +41,6 @@ export {
InputMultiSelect,
InputText,
Label,
MeasurementsPanel,
MeasurementTable,
Modal,
NavBar,
@ -56,6 +55,7 @@ export {
StudyListPagination,
StudyListTable,
StudyListTableRow,
StudySummary,
Svg,
Table,
TableBody,

View File

@ -28,12 +28,12 @@
},
"dependencies": {
"classnames": "^2.2.6",
"docz": "^2.2.0",
"gatsby": "2.19.24",
"gatsby-plugin-postcss": "^2.1.20",
"gatsby-plugin-react-svg": "^3.0.0",
"gatsby-plugin-sass": "^2.1.28",
"gatsby-theme-docz": "^2.2.0",
"docz": "^2.3.0",
"gatsby": "2.22.19",
"gatsby-plugin-postcss": "2.3.3",
"gatsby-plugin-react-svg": "3.0.0",
"gatsby-plugin-sass": "2.3.3",
"gatsby-theme-docz": "2.3.1",
"moment": "^2.24.0",
"node-sass": "^4.13.1",
"prop-types": "^15.7.2",

View File

@ -40,11 +40,3 @@
.invisible-scrollbar::-webkit-scrollbar-thumb:window-inactive {
@apply hidden;
}
.showExcludeButtonOnHover .excludeButton {
display: none;
}
.showExcludeButtonOnHover:hover .excludeButton {
display: block;
}

View File

@ -1,65 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { MeasurementTable } from '@ohif/ui';
const MeasurementsPanel = ({
descriptionData,
measurementTableData,
actionButtons,
}) => {
const { date, modality, description } = descriptionData;
return (
<>
<div className="overflow-y-auto overflow-x-hidden invisible-scrollbar">
<div className="p-2">
<div className="leading-none">
<span className="text-white text-base mr-2">{date}</span>
<span className="px-1 text-black bg-common-bright text-base rounded-sm font-bold">
{modality}
</span>
</div>
<div className="leading-none">
<span className="text-base text-primary-light">{description}</span>
</div>
</div>
<MeasurementTable
data={measurementTableData.data}
title={measurementTableData.title}
amount={measurementTableData.amount}
onClick={measurementTableData.onClick}
onEdit={measurementTableData.onEdit}
/>
</div>
<div className="p-4 flex justify-center">{actionButtons}</div>
</>
);
};
MeasurementsPanel.defaultProps = {
actionButtons: null,
};
MeasurementsPanel.propTypes = {
descriptionData: PropTypes.shape({
date: PropTypes.string,
modality: PropTypes.string,
description: PropTypes.string,
}).isRequired,
measurementTableData: PropTypes.shape({
title: PropTypes.string,
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
data: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
label: PropTypes.string,
displayText: PropTypes.string,
isActive: PropTypes.bool,
})
),
onClick: PropTypes.func,
onEdit: PropTypes.func,
}).isRequired,
actionButtons: PropTypes.node,
};
export default MeasurementsPanel;

View File

@ -1,101 +0,0 @@
---
name: Measurements Panel
menu: Data Display
route: components/measurementsPanel
---
import { useState } from 'react';
import { Playground, Props } from 'docz';
import {
SidePanel,
MeasurementsPanel,
ButtonGroup,
Button,
IconButton,
Icon,
} from '@ohif/ui';
# Measurements Panel
## Import
```javascript
import { MeasurementsPanel } from '@ohif/ui';
```
## Basic usage
<Playground>
{() => {
const [activeMeasurementItem, setActiveMeasurementItem] = useState(null);
const descriptionData = {
date: '07-Sep-2010',
modality: 'CT',
description: 'CHEST/ABD/PELVIS W CONTRAST',
};
const measurementTableData = {
title: 'Measurements',
amount: 10,
data: new Array(10).fill({}).map((el, i) => ({
id: i + 1,
label: 'Label short description',
displayText: '24.0 x 24.0 mm (S:4, I:22)',
isActive: activeMeasurementItem === i + 1,
})),
onClick: (id) => setActiveMeasurementItem((s) => (s === id ? null : id)),
onEdit: (id) => alert(`Edit: ${id}`),
};
return (
<div className="flex flex-row flex-no-wrap flex-1 items-stretch overflow-hidden w-full h-screen">
<div className="flex flex-1 h-100 overflow-hidden bg-primary-main items-center justify-center text-white">
CONTENT
</div>
<SidePanel
side="right"
iconName="list-bullets"
iconLabel="Measure"
componentLabel="Measurements"
defaultIsOpen={true}
>
<MeasurementsPanel
descriptionData={descriptionData}
measurementTableData={measurementTableData}
actionButtons={
<>
<ButtonGroup onClick={() => alert('Export')}>
<Button
className="text-white border-primary-main bg-black text-base py-2 px-2"
size="initial"
color="inherit"
>
Export
</Button>
<IconButton
className="bg-black border-primary-main px-2 text-white px-2"
color="inherit"
size="initial"
>
<Icon name="arrow-down" />
</IconButton>
</ButtonGroup>
<Button
className="text-white border border-primary-main bg-black text-base py-2 px-2 ml-2"
variant="outlined"
size="initial"
color="inherit"
onClick={() => alert('Create Report')}
>
Create Report
</Button>
</>
}
/>
</SidePanel>
</div>
);
}}
</Playground>
## Properties
<Props of={MeasurementsPanel} />

View File

@ -1,2 +0,0 @@
import MeasurementsPanel from './MeasurementsPanel';
export default MeasurementsPanel;

View File

@ -1,9 +1,9 @@
import React from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { Icon } from '@ohif/ui';
import { Button, Icon } from '@ohif/ui';
const Notification = ({ type, text, actionButtons }) => {
const Notification = ({ type, message, actions, onSubmit }) => {
const iconsByType = {
error: {
icon: 'info',
@ -35,12 +35,30 @@ const Notification = ({ type, text, actionButtons }) => {
const { icon, color } = getIconData();
return (
<div className="mx-2 mt-2 p-2 flex flex-col bg-common-bright rounded">
<div className="flex flex-col p-2 mx-2 mt-2 rounded bg-common-bright">
<div className="flex flex-grow">
<Icon name={icon} className={classnames('w-5', color)} />
<span className="text-base text-black ml-2">{text}</span>
<span className="ml-2 text-base text-black">{message}</span>
</div>
<div className="flex justify-end mt-2">
{actions.map((action, index) => {
const isFirst = index === 0;
const isPrimary = action.type === 'primary';
return (
<Button
key={index}
className={classnames({ 'ml-2': !isFirst })}
color={isPrimary ? 'primary' : undefined}
onClick={() => {
onSubmit(action.value);
}}
>
{action.text}
</Button>
);
})}
</div>
<div className="flex justify-end mt-2">{actionButtons}</div>
</div>
);
};
@ -51,8 +69,15 @@ Notification.defaultProps = {
Notification.propTypes = {
type: PropTypes.string,
text: PropTypes.string.isRequired,
actionButtons: PropTypes.node.isRequired,
message: PropTypes.string.isRequired,
actions: PropTypes.arrayOf(
PropTypes.shape({
text: PropTypes.string.isRequired,
value: PropTypes.any.isRequired,
type: PropTypes.oneOf(['primary', 'secondary', 'cancel']).isRequired,
})
).isRequired,
onSubmit: PropTypes.func.isRequired,
};
export default Notification;

View File

@ -5,7 +5,7 @@ route: components/notification
---
import { Playground, Props } from 'docz';
import { Notification, Button } from '@ohif/ui';
import { Notification } from '@ohif/ui';
# Notification
@ -22,23 +22,31 @@ import { Notification } from '@ohif/ui';
<Playground>
{() => {
const actionButtons = () => {
return (
<div>
<Button>No</Button>
<Button className="ml-2">No, do not ask again</Button>
<Button className="ml-2" color="primary">
Yes
</Button>
</div>
);
};
return (
<div className="p-4 w-full lg:w-2/3">
<Notification
text="Track all measurement for this series?"
message="Track all measurement for this series?"
type="info"
actionButtons={actionButtons()}
actions={[
{
type: 'cancel',
text: 'No',
value: 0,
},
{
type: 'secondary',
text: 'No, do not ask again',
value: -1,
},
{
type: 'primary',
text: 'Yes',
value: 1,
},
]}
onSubmit={value => {
window.alert(value);
}}
/>
</div>
);

View File

@ -25,6 +25,7 @@ const StudyBrowser = ({
onClickTab,
onClickStudy,
onClickThumbnail,
onClickUntrack,
}) => {
const [thumbnailActive, setThumbnailActive] = useState(null);
@ -58,15 +59,16 @@ const StudyBrowser = ({
<ThumbnailList
thumbnails={displaySets}
thumbnailActive={thumbnailActive}
onThumbnailClick={thumbnailId => {
onThumbnailClick={displaySetInstanceUID => {
setThumbnailActive(
thumbnailId === thumbnailActive ? null : thumbnailId
displaySetInstanceUID === thumbnailActive
? null
: displaySetInstanceUID
);
if (onClickThumbnail) {
// TODO: what is thumbnailId? Should pass display set instead
onClickThumbnail(thumbnailId);
}
onClickThumbnail(displaySetInstanceUID);
}}
onClickUntrack={displaySetInstanceUID => {
onClickUntrack(displaySetInstanceUID);
}}
/>
)}
@ -116,6 +118,7 @@ StudyBrowser.propTypes = {
onClickTab: PropTypes.func.isRequired,
onClickStudy: PropTypes.func,
onClickThumbnail: PropTypes.func,
onClickUntrack: PropTypes.func,
activeTabName: PropTypes.string.isRequired,
expandedStudyInstanceUIDs: PropTypes.arrayOf(PropTypes.string).isRequired,
tabs: PropTypes.arrayOf(
@ -164,4 +167,13 @@ StudyBrowser.propTypes = {
),
};
const noop = () => {};
StudyBrowser.defaultProps = {
onClickTab: noop,
onClickStudy: noop,
onClickThumbnail: noop,
onClickUntrack: noop,
};
export default StudyBrowser;

View File

@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
const StudySummary = ({ date, modality, description }) => {
return (
<div className="p-2">
<div className="leading-none">
<span className="mr-2 text-base text-white">{date}</span>
<span className="px-1 text-base font-bold text-black rounded-sm bg-common-bright">
{modality}
</span>
</div>
<div className="pt-2 text-base leading-none truncate text-primary-light ellipse">
{description}
</div>
</div>
);
};
StudySummary.propTypes = {
date: PropTypes.string.isRequired,
modality: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
};
export default StudySummary;

View File

@ -0,0 +1,3 @@
import StudySummary from './StudySummary.jsx';
export default StudySummary;

View File

@ -3,7 +3,12 @@ import PropTypes from 'prop-types';
import { Thumbnail, ThumbnailNoImage, ThumbnailTracked } from '@ohif/ui';
const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
const ThumbnailList = ({
thumbnails,
thumbnailActive,
onThumbnailClick,
onClickUntrack,
}) => {
return (
<div className="py-3 bg-black">
{thumbnails.map(
@ -53,6 +58,7 @@ const ThumbnailList = ({ thumbnails, thumbnailActive, onThumbnailClick }) => {
isTracked={isTracked}
isActive={isActive}
onClick={() => onThumbnailClick(displaySetInstanceUID)}
onClickUntrack={() => onClickUntrack(displaySetInstanceUID)}
/>
);
case 'thumbnailNoImage':
@ -107,6 +113,7 @@ ThumbnailList.propTypes = {
),
thumbnailActive: PropTypes.string,
onThumbnailClick: PropTypes.func,
onClickUntrack: PropTypes.func.isRequired,
};
export default ThumbnailList;

View File

@ -13,6 +13,7 @@ const ThumbnailTracked = ({
numInstances,
dragData,
onClick,
onClickUntrack,
viewportIdentificator,
isTracked,
isActive,
@ -22,7 +23,7 @@ const ThumbnailTracked = ({
return (
<div
className={classnames(
'flex flex-row flex-1 px-3 py-2 showExcludeButtonOnHover cursor-pointer outline-none',
'flex flex-row flex-1 px-3 py-2 cursor-pointer outline-none',
className
)}
>
@ -63,10 +64,9 @@ const ThumbnailTracked = ({
</Tooltip>
</div>
{isTracked && (
<Icon
name="cancel"
className="w-4 text-primary-active excludeButton"
/>
<div onClick={onClickUntrack}>
<Icon name="cancel" className="w-4 text-primary-active" />
</div>
)}
</div>
<Thumbnail
@ -102,6 +102,7 @@ ThumbnailTracked.propTypes = {
seriesNumber: PropTypes.number.isRequired,
numInstances: PropTypes.number.isRequired,
onClick: PropTypes.func.isRequired,
onClickUntrack: PropTypes.func.isRequired,
viewportIdentificator: PropTypes.string,
isTracked: PropTypes.bool,
isActive: PropTypes.bool.isRequired,

View File

@ -4,7 +4,7 @@ import { ViewportActionBar, Notification, Button } from '@ohif/ui';
const Viewport = ({ viewportIndex, onSeriesChange, studyData, children }) => {
return (
<div className="flex flex-col h-full relative">
<div className="relative flex flex-col h-full">
<div className="absolute top-0 left-0 w-full">
<ViewportActionBar
onSeriesChange={onSeriesChange}
@ -13,17 +13,28 @@ const Viewport = ({ viewportIndex, onSeriesChange, studyData, children }) => {
{/* TODO: NOTIFICATION API DEFINITION - OHIF-112 */}
<Notification
text="Track all measurement for this series?"
message="Track all measurement for this series?"
type="info"
actionButtons={
<div>
<Button>No</Button>
<Button className="ml-2">No, do not ask again</Button>
<Button className="ml-2" color="primary">
Yes
</Button>
</div>
}
actions={[
{
type: 'cancel',
text: 'No',
value: 0,
},
{
type: 'secondary',
text: 'No, do not ask again',
value: -1,
},
{
type: 'primary',
text: 'Yes',
value: 1,
},
]}
onSubmit={value => {
window.alert(value);
}}
/>
</div>

View File

@ -39,6 +39,7 @@ function ViewportPane({
// onInteraction...
// https://reactjs.org/docs/events.html#mouse-events
// https://stackoverflow.com/questions/8378243/catch-scrolling-event-on-overflowhidden-element
onMouseDown={onInteraction}
onClick={onInteraction}
onScroll={onInteraction}
onWheel={onInteraction}
@ -76,6 +77,6 @@ const noop = () => {};
ViewportPane.defaultProps = {
onInteraction: noop,
}
};
export default ViewportPane;

View File

@ -12,7 +12,6 @@ import InputLabelWrapper from './InputLabelWrapper';
import InputMultiSelect from './InputMultiSelect';
import InputText from './InputText';
import Label from './Label';
import MeasurementsPanel from './MeasurementsPanel';
import MeasurementTable from './MeasurementTable';
import Modal from './Modal';
import NavBar from './NavBar';
@ -21,12 +20,13 @@ import Select from './Select';
import SegmentationTable from './SegmentationTable';
import SidePanel from './SidePanel';
import StudyBrowser from './StudyBrowser';
import StudyItem from './StudyItem';
import StudyListExpandedRow from './StudyListExpandedRow';
import StudyListFilter from './StudyListFilter';
import StudyListPagination from './StudyListPagination';
import { StudyListTable, StudyListTableRow } from './StudyListTable';
import StudySummary from './StudySummary';
import Svg from './Svg';
import StudyItem from './StudyItem';
import StudyListFilter from './StudyListFilter';
import Table from './Table';
import TableBody from './TableBody';
import TableCell from './TableCell';
@ -61,7 +61,6 @@ export {
InputMultiSelect,
InputText,
Label,
MeasurementsPanel,
MeasurementTable,
Modal,
NavBar,
@ -76,6 +75,7 @@ export {
StudyListPagination,
StudyListTable,
StudyListTableRow,
StudySummary,
Svg,
Table,
TableBody,

View File

@ -1,9 +1,13 @@
import React from 'react';
import { DndProvider } from "react-dnd";
import PropTypes from 'prop-types';
import { DndProvider } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import TouchBackend from 'react-dnd-touch-backend';
const isTouchDevice = typeof window !== `undefined` && !!('ontouchstart' in window || navigator.maxTouchPoints);
// TODO: this is false when it should not be :thinking:
const isTouchDevice =
typeof window !== `undefined` &&
!!('ontouchstart' in window || navigator.maxTouchPoints);
/**
* Relevant:
@ -13,9 +17,11 @@ const isTouchDevice = typeof window !== `undefined` && !!('ontouchstart' in wind
* Docs:
* http://react-dnd.github.io/react-dnd/docs/api/drag-drop-context
*/
export default function DragAndDropProvider({children}) {
const backend = isTouchDevice ? TouchBackend : HTML5Backend;
const opts = isTouchDevice ? { enableMouseEvents: true } : {};
function DragAndDropProvider({ children }) {
const backend = HTML5Backend; // isTouchDevice ? TouchBackend : HTML5Backend;
const opts = {}; // isTouchDevice ? { enableMouseEvents: true } : {};
console.log('using... touch backend?', isTouchDevice);
return (
<DndProvider backend={backend} opts={opts}>
@ -23,3 +29,9 @@ export default function DragAndDropProvider({children}) {
</DndProvider>
);
}
DragAndDropProvider.propTypes = {
children: PropTypes.any,
};
export default DragAndDropProvider;

View File

@ -7,10 +7,20 @@ import React, {
} from 'react';
import PropTypes from 'prop-types';
const DEFAULT_OPTIONS = {
content: null,
contentProps: null,
customClassName: null,
const DEFAULT_STATE = {
viewportIndex: null,
message: undefined,
type: 'info', // "error" | "warning" | "info" | "success"
actions: undefined, // array of { type, text, value }
// dismissable?
// blockInteraction (single viewport? allViewports? everything?)
// TODO: Buttons --> type/color? text? value?
onSubmit: () => {
console.log('btn value?');
},
onDismiss: () => {
console.log('dismiss? -1');
},
};
const ViewportDialogContext = createContext(null);
@ -18,42 +28,26 @@ const { Provider } = ViewportDialogContext;
export const useViewportDialog = () => useContext(ViewportDialogContext);
const ViewportDialogProvider = ({
children,
dialog: Dialog,
service,
viewportIndex,
}) => {
const [options, setOptions] = useState(DEFAULT_OPTIONS);
const show = useCallback((props) => setOptions({ ...options, ...props }), [
options,
]);
const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), []);
const ViewportDialogProvider = ({ children, service }) => {
const [viewportDialogState, setViewportDialogState] = useState(DEFAULT_STATE);
const show = useCallback(
params => setViewportDialogState({ ...viewportDialogState, ...params }),
[viewportDialogState]
);
const hide = useCallback(() => setViewportDialogState(DEFAULT_STATE), []);
useEffect(() => {
if (service) {
service.setServiceImplementation({ hide, show, viewportIndex });
service.setServiceImplementation({
hide,
show,
});
}
}, [hide, service, show, viewportIndex]);
const {
content: ViewportDialogContent,
contentProps,
customClassName,
} = options;
}, [hide, service, show]);
return (
<Provider value={{ show, hide }}>
<div className="relative w-full h-full">
{ViewportDialogContent && (
<Dialog className={customClassName}>
<ViewportDialogContent {...contentProps} show={show} hide={hide} />
</Dialog>
)}
{children}
</div>
<Provider value={[viewportDialogState, { show, hide }]}>
{children}
</Provider>
);
};
@ -64,16 +58,9 @@ ViewportDialogProvider.propTypes = {
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
/** dialog component */
dialog: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
PropTypes.func,
]).isRequired,
service: PropTypes.shape({
setServiceImplementation: PropTypes.func,
}),
viewportIndex: PropTypes.number,
};
export default ViewportDialogProvider;

View File

@ -28,19 +28,26 @@ component across all application.
const ViewportNotification = ({ hide }) => {
return (
<Notification
text="Track all measurement for this series?"
message="Track all measurement for this series?"
type="info"
actionButtons={
<div>
<Button onClick={hide}>No</Button>
<Button onClick={hide} className="ml-2">
No, do not ask again
</Button>
<Button onClick={hide} className="ml-2" color="primary">
Yes
</Button>
</div>
}
actions={[
{
type: 'cancel',
text: 'No',
value: 0,
},
{
type: 'secondary',
text: 'No, do not ask again',
value: -1,
},
{
type: 'primary',
text: 'Yes',
value: 1,
},
]}
onSubmit={value => { window.alert(value); }}
/>
);
};
@ -88,17 +95,26 @@ component across all application.
const ViewportNotification = ({ hide }) => {
return (
<Notification
text="Track all measurement for this series?"
message="Track all measurement for this series?"
type="info"
actionButtons={
<div>
<Button onClick={hide}>No</Button>
<Button className="ml-2">No, do not ask again</Button>
<Button className="ml-2" color="primary">
Yes
</Button>
</div>
}
actions={[
{
type: 'cancel',
text: 'No',
value: 0,
},
{
type: 'secondary',
text: 'No, do not ask again',
value: -1,
},
{
type: 'primary',
text: 'Yes',
value: 1,
},
]}
onSubmit={value => { window.alert(value); }}
/>
);
};

View File

@ -10,7 +10,6 @@ import {
NavBar,
SidePanel,
Svg,
MeasurementsPanel,
SegmentationTable,
ButtonGroup,
Button,
@ -125,10 +124,22 @@ import { tabs } from './studyBrowserMockData';
label: 'Measurements',
name: 'measurements',
content: (
<MeasurementsPanel
descriptionData={descriptionData}
measurementTableData={measurementTableData}
actionButtons={
<>
<div className="overflow-x-hidden overflow-y-auto invisible-scrollbar">
<StudySummary
date={descriptionData.date}
modality={descriptionData.modality}
description={descriptionData.description}
/>
<MeasurementTable
title="Measurements"
amount={measurementTableData.data.length}
data={measurementTableData.data}
onClick={() => {}}
onEdit={id => alert(`Edit: ${id}`)}
/>
</div>
<div className="flex justify-center p-4">
<React.Fragment>
<ButtonGroup onClick={() => alert('Export')}>
<Button
@ -156,8 +167,8 @@ import { tabs } from './studyBrowserMockData';
Create Report
</Button>
</React.Fragment>
}
/>
</div>
</>
)
}}
/>

View File

@ -19,14 +19,12 @@
"scripts": {
"build:package": "cross-env NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.commonjs.js --progress",
"build:viewer": "cross-env NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js --progress",
"build:viewer:lesion-tracker": "cross-env ENTRY_TARGET=index-lesion-tracker.js NODE_ENV=production node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js --progress",
"build:viewer:ci": "cross-env NODE_ENV=production PUBLIC_URL=/pwa/ APP_CONFIG=config/netlify.js QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --config .webpack/webpack.pwa.js",
"build:viewer:demo": "cross-env NODE_ENV=production APP_CONFIG=config/demo.js HTML_TEMPLATE=rollbar.html QUICK_BUILD=true node --max_old_space_size=4096 ./../../node_modules/webpack/bin/webpack.js --progress --config .webpack/webpack.pwa.js",
"build:viewer:package": "yarn run build:package",
"dev": "cross-env NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
"dev:orthanc": "cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker_nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js --watch",
"dev:viewer": "yarn run dev",
"dev:viewer:lesion-tracker": "cross-env ENTRY_TARGET=index-lesion-tracker.js NODE_ENV=development webpack-dev-server --config .webpack/webpack.pwa.js --watch",
"start": "yarn run dev",
"test:e2e": "cypress open",
"test:e2e:ci": "percy exec -- cypress run --config video=false --record --browser chrome --spec 'cypress/integration/visual-regression/**/*'",

View File

@ -6,8 +6,10 @@ import {
DialogProvider,
Modal,
ModalProvider,
Notification,
SnackbarProvider,
ThemeWrapper,
ViewportDialogProvider,
ViewportGridProvider,
} from '@ohif/ui';
// Viewer Project
@ -49,11 +51,11 @@ function App({ config, defaultExtensions }) {
extensionManager,
servicesManager
);
const {
UIDialogService,
UIModalService,
UINotificationService,
UIViewportDialogService,
} = servicesManager.services;
// A UI Service may need to use the ViewportGrid context
@ -63,13 +65,33 @@ function App({ config, defaultExtensions }) {
switch (action.type) {
case 'SET_ACTIVE_VIEWPORT_INDEX':
return { ...state, ...{ activeViewportIndex: action.payload } };
case 'SET_DISPLAYSET_FOR_VIEWPORT':
case 'SET_DISPLAYSET_FOR_VIEWPORT': {
const { viewportIndex, displaySetInstanceUID } = action.payload;
const viewports = state.viewports.slice();
viewports[viewportIndex] = { displaySetInstanceUID };
return { ...state, ...{ viewports } };
}
case 'SET_LAYOUT': {
const { numCols, numRows } = action.payload;
const numPanes = numCols * numRows;
const viewports = state.viewports.slice();
const activeViewportIndex =
state.activeViewportIndex >= numPanes ? 0 : state.activeViewportIndex;
while (viewports.length < numPanes) {
viewports.push({});
}
while (viewports.length > numPanes) {
viewports.pop();
}
return {
...state,
...{ activeViewportIndex, numCols, numRows, viewports },
};
}
default:
return action.payload;
}
@ -88,13 +110,15 @@ function App({ config, defaultExtensions }) {
}}
reducer={viewportGridReducer}
>
<SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={Modal} service={UIModalService}>
{appRoutes}
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
<ViewportDialogProvider service={UIViewportDialogService}>
<SnackbarProvider service={UINotificationService}>
<DialogProvider service={UIDialogService}>
<ModalProvider modal={Modal} service={UIModalService}>
{appRoutes}
</ModalProvider>
</DialogProvider>
</SnackbarProvider>
</ViewportDialogProvider>
</ViewportGridProvider>
</ThemeWrapper>
</Router>

View File

@ -6,6 +6,7 @@ import {
UINotificationService,
UIModalService,
UIDialogService,
UIViewportDialogService,
MeasurementService,
DisplaySetService,
ToolBarSerivce,
@ -46,6 +47,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
UINotificationService,
UIModalService,
UIDialogService,
UIViewportDialogService,
MeasurementService,
DisplaySetService,
ToolBarSerivce,

3814
yarn.lock

File diff suppressed because it is too large Load Diff