Support for additional "tracking" mode

This commit is contained in:
dannyrb 2020-05-26 09:03:58 -04:00
parent fae21bed9c
commit d557acbb40
28 changed files with 1189 additions and 22 deletions

View File

@ -0,0 +1,8 @@
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
};

View File

@ -0,0 +1,44 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const pkg = require('./../package.json');
const ROOT_DIR = path.join(__dirname, './..');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
devtool: 'source-map',
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: true,
},
output: {
path: ROOT_DIR,
library: 'OHIFExtDefault',
libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main,
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
});
};

View File

@ -0,0 +1,40 @@
{
"name": "@ohif/extension-measurement-tracking",
"version": "0.0.1",
"description": "Tracking features and functionality for basic image viewing",
"author": "OHIF Core Team",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/index.umd.js",
"module": "src/index.js",
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=10",
"npm": ">=6",
"yarn": ">=1.19.1"
},
"files": [
"dist",
"README.md"
],
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
"dev:dicom-pdf": "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.13.1",
"react-dom": "^16.13.1",
"webpack": "^4.0.0",
"dcmjs": "^0.12.4"
},
"dependencies": {
"@babel/runtime": "7.7.6"
}
}

View File

@ -0,0 +1,74 @@
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="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>
);
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)),
onClick: () => {},
onEdit: id => alert(`Edit: ${id}`),
};
return (
<MeasurementsPanel
descriptionData={descriptionData}
measurementTableData={measurementTableData}
actionButtons={actionButtons}
/>
);
}

View File

@ -0,0 +1,284 @@
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { StudyBrowser, useImageViewer } from '@ohif/ui';
/**
*
* @param {*} param0
*/
function PanelStudyBrowser({
DisplaySetService,
getImageSrc,
getStudiesForPatientByStudyInstanceUID,
requestDisplaySetCreationForStudy,
dataSource,
}) {
// Normally you nest the components so the tree isn't so deep, and the data
// doesn't have to have such an intense shape. This works well enough for now.
// Tabs --> Studies --> DisplaySets --> Thumbnails
const [{ StudyInstanceUIDs }, dispatch] = useImageViewer();
const [activeTabName, setActiveTabName] = useState('primary');
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([]);
const [studyDisplayList, setStudyDisplayList] = useState([]);
const [displaySets, setDisplaySets] = useState([]);
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
// ~~ studyDisplayList
useEffect(() => {
// Fetch all studies for the patient in each primary study
async function fetchStudiesForPatient(StudyInstanceUID) {
const qidoStudiesForPatient =
(await getStudiesForPatientByStudyInstanceUID(StudyInstanceUID)) || [];
// TODO: This should be "naturalized DICOM JSON" studies
const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient);
const actuallyMappedStudies = mappedStudies.map(qidoStudy => {
return {
studyInstanceUid: qidoStudy.StudyInstanceUID,
date: qidoStudy.StudyDate,
description: qidoStudy.StudyDescription,
modalities: qidoStudy.ModalitiesInStudy,
numInstances: qidoStudy.NumInstances,
// displaySets: []
};
});
setStudyDisplayList(actuallyMappedStudies);
}
StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
}, [StudyInstanceUIDs, getStudiesForPatientByStudyInstanceUID]);
// ~~ Initial Thumbnails
useEffect(() => {
const currentDisplaySets = DisplaySetService.activeDisplaySets;
currentDisplaySets.forEach(async dSet => {
const newImageSrcEntry = {};
const displaySet = DisplaySetService.getDisplaySetByUID(
dSet.displaySetInstanceUID
);
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
const imageId = imageIds[Math.floor(imageIds.length / 2)];
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(imageId);
setThumbnailImageSrcMap(prevState => {
return { ...prevState, ...newImageSrcEntry };
});
});
}, []);
// ~~ displaySets
useEffect(() => {
// TODO: Are we sure `activeDisplaySets` will always be accurate?
const currentDisplaySets = DisplaySetService.activeDisplaySets;
const mappedDisplaySets = _mapDisplaySets(
currentDisplaySets,
thumbnailImageSrcMap
);
setDisplaySets(mappedDisplaySets);
}, [thumbnailImageSrcMap]);
// ~~ subscriptions --> displaySets
useEffect(() => {
// DISPLAY_SETS_ADDED returns an array of DisplaySets that were added
const SubscriptionDisplaySetsAdded = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
newDisplaySets => {
newDisplaySets.forEach(async dSet => {
const newImageSrcEntry = {};
const displaySet = DisplaySetService.getDisplaySetByUID(
dSet.displaySetInstanceUID
);
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
const imageId = imageIds[Math.floor(imageIds.length / 2)];
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
imageId
);
setThumbnailImageSrcMap(prevState => {
return { ...prevState, ...newImageSrcEntry };
});
});
}
);
// TODO: Will this always hold _all_ the displaySets we care about?
// DISPLAY_SETS_CHANGED returns `DisplaySerService.activeDisplaySets`
const SubscriptionDisplaySetsChanged = DisplaySetService.subscribe(
DisplaySetService.EVENTS.DISPLAY_SETS_CHANGED,
changedDisplaySets => {
const mappedDisplaySets = _mapDisplaySets(
changedDisplaySets,
thumbnailImageSrcMap
);
setDisplaySets(mappedDisplaySets);
}
);
return () => {
SubscriptionDisplaySetsAdded.unsubscribe();
SubscriptionDisplaySetsChanged.unsubscribe();
};
}, []);
const tabs = _createStudyBrowserTabs(
StudyInstanceUIDs,
studyDisplayList,
displaySets
);
// TODO: Should not fire this on "close"
function _handleStudyClick(StudyInstanceUID) {
const shouldCollapseStudy = expandedStudyInstanceUIDs.includes(
StudyInstanceUID
);
const updatedExpandedStudyInstanceUIDs = shouldCollapseStudy
// eslint-disable-next-line prettier/prettier
? [...expandedStudyInstanceUIDs.filter(stdyUid => stdyUid !== StudyInstanceUID)]
: [...expandedStudyInstanceUIDs, StudyInstanceUID];
setExpandedStudyInstanceUIDs(updatedExpandedStudyInstanceUIDs);
if (!shouldCollapseStudy) {
requestDisplaySetCreationForStudy(DisplaySetService, StudyInstanceUID);
}
}
return (
<StudyBrowser
tabs={tabs}
activeTabName={activeTabName}
expandedStudyInstanceUIDs={expandedStudyInstanceUIDs}
onClickStudy={_handleStudyClick}
onClickTab={clickedTabName => {
setActiveTabName(clickedTabName);
}}
/>
);
}
PanelStudyBrowser.propTypes = {
DisplaySetService: PropTypes.shape({
EVENTS: PropTypes.object.isRequired,
activeDisplaySets: PropTypes.arrayOf(PropTypes.object).isRequired,
getDisplaySetByUID: PropTypes.func.isRequired,
hasDisplaySetsForStudy: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
}).isRequired,
dataSource: PropTypes.shape({
getImageIdsForDisplaySet: PropTypes.func.isRequired,
}).isRequired,
getImageSrc: PropTypes.func.isRequired,
getStudiesForPatientByStudyInstanceUID: PropTypes.func.isRequired,
requestDisplaySetCreationForStudy: PropTypes.func.isRequired,
};
export default PanelStudyBrowser;
/**
* Maps from the DataSource's format to a naturalized object
*
* @param {*} studies
*/
function _mapDataSourceStudies(studies) {
return studies.map(study => {
// TODO: Why does the data source return in this format?
return {
AccessionNumber: study.accession,
StudyDate: study.date,
StudyDescription: study.description,
NumInstances: study.instances,
ModalitiesInStudy: study.modalities,
PatientID: study.mrn,
PatientName: study.patientName,
StudyInstanceUID: study.studyInstanceUid,
StudyTime: study.time,
};
});
}
function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
return displaySets.map(ds => {
const imageSrc = thumbnailImageSrcMap[ds.displaySetInstanceUID];
return {
displaySetInstanceUID: ds.displaySetInstanceUID,
description: ds.SeriesDescription,
seriesNumber: ds.SeriesNumber,
modality: ds.Modality,
date: ds.SeriesDate,
numInstances: ds.numImageFrames,
StudyInstanceUID: ds.StudyInstanceUID,
componentType: 'thumbnail', // 'thumbnailNoImage' || 'thumbnailTracked' // TODO: PUT THIS SOMEWHERE ELSE
imageSrc,
dragData: {
type: 'displayset',
displaySetInstanceUID: ds.displaySetInstanceUID,
// .. Any other data to pass
},
};
});
}
/**
*
* @param {string[]} primaryStudyInstanceUIDs
* @param {object[]} studyDisplayList
* @param {string} studyDisplayList.studyInstanceUid
* @param {string} studyDisplayList.date
* @param {string} studyDisplayList.description
* @param {string} studyDisplayList.modalities
* @param {number} studyDisplayList.numInstances
* @param {object[]} displaySets
* @returns tabs - The prop object expected by the StudyBrowser component
*/
function _createStudyBrowserTabs(
primaryStudyInstanceUIDs,
studyDisplayList,
displaySets
) {
const primaryStudies = [];
const recentStudies = [];
const allStudies = [];
studyDisplayList.forEach(study => {
const displaySetsForStudy = displaySets.filter(
ds => ds.StudyInstanceUID === study.studyInstanceUid
);
const tabStudy = Object.assign({}, study, {
displaySets: displaySetsForStudy,
});
if (primaryStudyInstanceUIDs.includes(study.studyInstanceUid)) {
primaryStudies.push(tabStudy);
} else {
// TODO: Filter allStudies to dates within one year of current date
recentStudies.push(tabStudy);
allStudies.push(tabStudy);
}
});
const tabs = [
{
name: 'primary',
label: 'Primary',
studies: primaryStudies,
},
{
name: 'recent',
label: 'Recent',
studies: recentStudies,
},
{
name: 'all',
label: 'All',
studies: allStudies,
},
];
return tabs;
}

View File

@ -0,0 +1,19 @@
/**
* @param {*} cornerstone
* @param {*} imageId
*/
function getImageSrcFromImageId(cornerstone, imageId) {
return new Promise((resolve, reject) => {
cornerstone
.loadAndCacheImage(imageId)
.then(image => {
const canvas = document.createElement('canvas');
cornerstone.renderToCanvas(canvas, image);
resolve(canvas.toDataURL());
})
.catch(reject);
});
}
export default getImageSrcFromImageId;

View File

@ -0,0 +1,23 @@
async function getStudiesForPatientByStudyInstanceUID(
dataSource,
StudyInstanceUID
) {
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
// Data _could_ be here from route query, or if using JSON data source
// We could also force this to "await" these values being available in the DICOMStore?
// Kind of like promise fulfillment in cornerstone-wado-image-loader when there are multiple
// outgoing requests for the same data
const getStudyResult = await dataSource.query.studies.search({
studyInstanceUid: StudyInstanceUID,
});
// TODO: To Erik's point, the data source likely shouldn't deviate from
// Naturalized DICOM JSON when returning. It makes things like this awkward (mrn)
if (getStudyResult && getStudyResult.length && getStudyResult[0].mrn) {
return dataSource.query.studies.search({
patientId: getStudyResult[0].mrn,
});
}
}
export default getStudiesForPatientByStudyInstanceUID;

View File

@ -0,0 +1,78 @@
import React from 'react';
import PropTypes from 'prop-types';
//
import PanelStudyBrowser from './PanelStudyBrowser';
import getImageSrcFromImageId from './getImageSrcFromImageId';
import getStudiesForPatientByStudyInstanceUID from './getStudiesForPatientByStudyInstanceUID';
import requestDisplaySetCreationForStudy from './requestDisplaySetCreationForStudy';
/**
* Wraps the PanelStudyBrowser and provides features afforded by managers/services
*
* @param {object} params
* @param {object} commandsManager
* @param {object} extensionManager
*/
function WrappedPanelStudyBrowser({
commandsManager,
extensionManager,
servicesManager,
}) {
// TODO: This should be made available a different way; route should have
// already determined our datasource
const dataSource = extensionManager.getDataSources('dicomweb')[0];
const _getStudiesForPatientByStudyInstanceUID = getStudiesForPatientByStudyInstanceUID.bind(
null,
dataSource
);
const _getImageSrcFromImageId = _createGetImageSrcFromImageIdFn(
commandsManager.getCommand.bind(commandsManager)
);
const _requestDisplaySetCreationForStudy = requestDisplaySetCreationForStudy.bind(
null,
dataSource
);
return (
<PanelStudyBrowser
DisplaySetService={servicesManager.services.DisplaySetService}
dataSource={dataSource}
getImageSrc={_getImageSrcFromImageId}
getStudiesForPatientByStudyInstanceUID={
_getStudiesForPatientByStudyInstanceUID
}
requestDisplaySetCreationForStudy={_requestDisplaySetCreationForStudy}
/>
);
}
/**
* Grabs cornerstone library reference using a dependent command from
* the @ohif/extension-cornerstone extension. Then creates a helper function
* that can take an imageId and return an image src.
*
* @param {func} getCommand - CommandManager's getCommand method
* @returns {func} getImageSrcFromImageId - A utility function powered by
* cornerstone
*/
function _createGetImageSrcFromImageIdFn(getCommand) {
try {
const command = getCommand('getCornerstoneLibraries', 'VIEWER');
if (!command) {
return;
}
const { cornerstone } = command.commandFn();
return getImageSrcFromImageId.bind(null, cornerstone);
} catch (ex) {
throw new Error('Required command not found');
}
}
WrappedPanelStudyBrowser.propTypes = {
commandsManager: PropTypes.object.isRequired,
extensionManager: PropTypes.object.isRequired,
servicesManager: PropTypes.object.isRequired,
};
export default WrappedPanelStudyBrowser;

View File

@ -0,0 +1,14 @@
function requestDisplaySetCreationForStudy(
dataSource,
DisplaySetService,
StudyInstanceUID
) {
// TODO: is this already short-circuited by the map of Retrieve promises?
if (DisplaySetService.hasDisplaySetsForStudy(StudyInstanceUID)) {
return;
}
dataSource.retrieveSeriesMetadata({ StudyInstanceUID });
}
export default requestDisplaySetCreationForStudy;

View File

@ -0,0 +1,4 @@
import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
import PanelMeasurementTableTracking from './PanelMeasurementTableTracking';
export { PanelMeasurementTableTracking, PanelStudyBrowserTracking };

View File

@ -0,0 +1,193 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import cornerstone from 'cornerstone-core';
import CornerstoneViewport from 'react-cornerstone-viewport';
import OHIF from '@ohif/core';
import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle';
// const cine = viewportSpecificData.cine;
// isPlaying = cine.isPlaying === true;
// frameRate = cine.cineFrameRate || frameRate;
const { StackManager } = OHIF.utils;
class OHIFCornerstoneViewport extends Component {
state = {
viewportData: null,
};
static defaultProps = {
customProps: {},
};
static propTypes = {
displaySet: PropTypes.object,
viewportIndex: PropTypes.number,
dataSource: PropTypes.object,
children: PropTypes.node,
customProps: PropTypes.object,
};
static name = 'OHIFCornerstoneViewport';
static init() {
console.log('OHIFCornerstoneViewport init()');
}
// TODO: Still needed? Better way than import `OHIF` and destructure?
// Why is this managed by `core`?
static destroy() {
console.log('OHIFCornerstoneViewport destroy()');
StackManager.clearStacks();
}
/**
* Obtain the CornerstoneTools Stack for the specified display set.
*
* @param {Object} displaySet
* @param {Object} dataSource
* @return {Object} CornerstoneTools Stack
*/
static getCornerstoneStack(displaySet, dataSource) {
const { frameIndex } = displaySet;
// Get stack from Stack Manager
const storedStack = StackManager.findOrCreateStack(displaySet, dataSource);
// Clone the stack here so we don't mutate it
const stack = Object.assign({}, storedStack);
stack.currentImageIdIndex = frameIndex;
return stack;
}
getViewportData = async displaySet => {
let viewportData;
const { dataSource } = this.props;
const stack = OHIFCornerstoneViewport.getCornerstoneStack(
displaySet,
dataSource
);
viewportData = {
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
stack,
};
return viewportData;
};
setStateFromProps() {
const { displaySet } = this.props;
const {
StudyInstanceUID,
displaySetInstanceUID,
sopClassUids,
} = displaySet;
if (!StudyInstanceUID || !displaySetInstanceUID) {
return;
}
if (sopClassUids && sopClassUids.length > 1) {
console.warn(
'More than one SOPClassUID in the same series is not yet supported.'
);
}
this.getViewportData(displaySet).then(viewportData => {
this.setState({
viewportData,
});
});
}
componentDidMount() {
this.setStateFromProps();
}
componentDidUpdate(prevProps) {
const { displaySet } = this.props;
const prevDisplaySet = prevProps.displaySet;
if (
displaySet.displaySetInstanceUID !==
prevDisplaySet.displaySetInstanceUID ||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
displaySet.frameIndex !== prevDisplaySet.frameIndex
) {
this.setStateFromProps();
}
}
render() {
let childrenWithProps = null;
if (!this.state.viewportData) {
return null;
}
const { viewportIndex } = this.props;
const {
imageIds,
currentImageIdIndex,
// If this comes from the instance, would be a better default
// `FrameTime` in the instance
// frameRate = 0,
} = this.state.viewportData.stack;
// TODO: Does it make more sense to use Context?
if (this.props.children && this.props.children.length) {
childrenWithProps = this.props.children.map((child, index) => {
return (
child &&
React.cloneElement(child, {
viewportIndex: this.props.viewportIndex,
key: index,
})
);
});
}
const debouncedNewImageHandler = debounce(
({ currentImageIdIndex, sopInstanceUid }) => {
const { displaySet } = this.props;
const { StudyInstanceUID } = displaySet;
if (currentImageIdIndex > 0) {
this.props.onNewImage({
StudyInstanceUID,
SOPInstanceUID: sopInstanceUid,
frameIndex: currentImageIdIndex,
activeViewportIndex: viewportIndex,
});
}
},
700
);
return (
<>
<CornerstoneViewport
viewportIndex={viewportIndex}
imageIds={imageIds}
imageIdIndex={currentImageIdIndex}
onNewImage={debouncedNewImageHandler}
// TODO: ViewportGrid Context?
isActive={true} // todo
isStackPrefetchEnabled={true} // todo
isPlaying={false}
frameRate={24}
/>
{childrenWithProps}
</>
);
}
}
export default OHIFCornerstoneViewport;

View File

@ -0,0 +1,64 @@
// SEE:
// https://github.com/OHIF/Viewers/blob/b58aa4575ab72fe3f493cc5a4261b4f8256516ab/platform/viewer/src/appExtensions/MeasurementsPanel/index.js#L18-L49
import React from 'react';
import { useViewportGrid } from '@ohif/ui';
function getCommandsModule({ servicesManager }) {
const { UIDialogService } = servicesManager.services;
const definitions = {
toggleLayoutSelectionDialog: {
commandFn: () => {
if (!UIDialogService) {
window.alert('Unable to show dialog; no UI Dialog Service available.');
return;
}
// TODO: use SimpleDialog component
// TODO: update position on window resize
// TODO: Expand service API to check if dialog w/ ID is already open
// TODO: Import and call `useViewportGrid`
UIDialogService.dismiss({ id: 'layoutSelection' });
UIDialogService.create({
id: 'layoutSelection',
centralize: true,
isDraggable: false,
showOverlay: true,
content: Test,
});
},
storeContexts: [],
options: {},
context: 'VIEWER',
},
}
return {
definitions,
defaultContext: 'VIEWER',
};
}
function Test() {
const [
{ numCols, numRows, activeViewportIndex, viewports },
dispatch,
] = useViewportGrid();
return <div
onClick={() => {
dispatch({ type: '', payload: {
numCols: 3,
numRows: 3,
activeViewportIndex: 0,
viewports: [],
}})
}}
style={{ color: 'white' }}
>
Hello World!
</div>
}
export default getCommandsModule;

View File

@ -0,0 +1,38 @@
import React, { useState } from 'react';
import { ViewModelContext } from '@ohif/core';
const HelloWorldContext = React.createContext({
message: 'HelloWorldContextTesting',
setMessage: () => {},
});
HelloWorldContext.displayName = 'HelloWorldContext';
function HelloWorldContextProvider({ children }) {
const [message, setMessage] = useState('HelloWorldContextTesting');
return (
<HelloWorldContext.Provider
value={{
message,
setMessage,
}}
>
{children}
</HelloWorldContext.Provider>
);
}
function getContextModule() {
return [
{
name: 'HelloWorldContext',
context: HelloWorldContext,
provider: HelloWorldContextProvider,
},
];
}
export { HelloWorldContext };
export default getContextModule;

View File

@ -0,0 +1,41 @@
import {
PanelStudyBrowserTracking,
PanelMeasurementTableTracking,
} from './Panels';
// TODO:
// - No loading UI exists yet
// - cancel promises when component is destroyed
// - show errors in UI for thumbnails if promise fails
function getPanelModule({
commandsManager,
extensionManager,
servicesManager,
}) {
return [
{
name: 'seriesList',
iconName: 'group-layers',
iconLabel: 'Studies',
label: 'Studies',
component: PanelStudyBrowserTracking.bind(null, {
commandsManager,
extensionManager,
servicesManager,
}),
},
{
name: 'measure',
iconName: 'list-bullets',
iconLabel: 'Measure',
label: 'Measurements',
component: PanelMeasurementTableTracking.bind(null, {
commandsManager,
extensionManager,
servicesManager,
}),
},
];
}
export default getPanelModule;

View File

@ -0,0 +1,31 @@
import React from 'react';
const Component = React.lazy(() => {
return import('./Viewports/OHIFCornerstoneViewport');
});
const OHIFCornerstoneViewport = props => {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<Component {...props} />
</React.Suspense>
);
};
function getViewportModule({ commandsManager }) {
const ExtendedOHIFCornerstoneViewport = props => {
const onNewImageHandler = jumpData => {
alert('here we are!')
commandsManager.runCommand('jumpToImage', jumpData);
};
return (
<OHIFCornerstoneViewport {...props} onNewImage={onNewImageHandler} />
);
};
return [
{ name: 'cornerstone-tracked', component: ExtendedOHIFCornerstoneViewport },
];
};
export default getViewportModule;

View File

@ -0,0 +1,15 @@
import getCommandsModule from './getCommandsModule.js';
import getContextModule from './getContextModule.js';
import getPanelModule from './getPanelModule.js';
import getViewportModule from './getViewportModule.js';
export default {
/**
* Only required property. Should be a unique value across all extensions.
*/
id: 'org.ohif.measurement-tracking',
getCommandsModule,
getContextModule,
getPanelModule,
getViewportModule,
};

View File

@ -0,0 +1,8 @@
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
return webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
};

View File

@ -0,0 +1,44 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.commonjs.js');
const pkg = require('./../package.json');
const ROOT_DIR = path.join(__dirname, './..');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
devtool: 'source-map',
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: true,
},
output: {
path: ROOT_DIR,
library: 'OHIFExtCornerstone',
libraryTarget: 'umd',
libraryExport: 'default',
filename: pkg.main,
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
],
});
};

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Open Health Imaging Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1 @@
module.exports = require('../../babel.config.js');

View File

@ -0,0 +1,35 @@
{
"name": "@ohif/mode-longitudinal",
"version": "0.0.1",
"description": "Longitudinal Workflow",
"author": "OHIF",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/index.umd.js",
"module": "src/index.js",
"engines": {
"node": ">=10",
"npm": ">=6",
"yarn": ">=1.16.0"
},
"files": [
"dist",
"README.md"
],
"publishConfig": {
"access": "public"
},
"scripts": {
"dev": "cross-env NODE_ENV=development webpack --config .webpack/webpack.dev.js --watch --debug --output-pathinfo",
"dev:cornerstone": "yarn run dev",
"build": "cross-env NODE_ENV=production webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"start": "yarn run dev",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage"
},
"peerDependencies": {},
"dependencies": {
"@babel/runtime": "7.7.6"
}
}

View File

@ -0,0 +1,98 @@
export default function mode({ modeConfiguration }) {
return {
// TODO: We're using this as a route segment
// We should not be.
id: 'longitudinal-workflow',
validationTags: {
study: [],
series: [],
},
isValidMode: (studyTags, seriesTags) => {
// All series are welcome in this mode!
return true;
},
routes: [
{
path: 'longitudinal',
init: ({ servicesManager, extensionManager }) => {
const { ToolBarService } = servicesManager.services;
ToolBarService.init(extensionManager);
ToolBarService.addButtons([
{
id: 'Zoom',
namespace: 'org.ohif.cornerstone.toolbarModule.Zoom',
},
{
id: 'Levels',
namespace: 'org.ohif.cornerstone.toolbarModule.Wwwc',
},
{
id: 'Pan',
namespace: 'org.ohif.cornerstone.toolbarModule.Pan',
},
{
id: 'Capture',
namespace: 'org.ohif.cornerstone.toolbarModule.Capture',
},
{
id: 'Layout',
namespace: 'org.ohif.default.toolbarModule.Layout',
},
{
id: 'Annotate',
namespace: 'org.ohif.cornerstone.toolbarModule.Annotate',
},
{
id: 'Bidirectional',
namespace: 'org.ohif.cornerstone.toolbarModule.Bidirectional',
},
{
id: 'Ellipse',
namespace: 'org.ohif.cornerstone.toolbarModule.Ellipse',
},
{
id: 'Length',
namespace: 'org.ohif.cornerstone.toolbarModule.Length',
},
]);
// Could import layout selector here from org.ohif.default (when it exists!)
ToolBarService.setToolBarLayout([
// Primary
{
tools: ['Zoom', 'Levels', 'Pan', 'Capture', 'Layout'],
moreTools: ['Zoom'],
},
// Secondary
{
tools: ['Annotate', 'Bidirectional', 'Ellipse', 'Length'],
},
]);
},
layoutTemplate: ({ routeProps }) => {
return {
id: 'org.ohif.default.layoutTemplateModule.viewerLayout',
props: {
// named slots
leftPanels: ['org.ohif.default.panelModule.seriesList'],
// TODO: Should be optional, or required to pass empty array for slots?
rightPanels: [], // // ['org.ohif.default.panelModule.measure'],
viewports: [
{
namespace: 'org.ohif.measurement-tracking.viewportModule.cornerstone-tracked',
displaySetsToDisplay: [
'org.ohif.default.sopClassHandlerModule.stack',
],
},
],
},
};
},
},
],
extensions: ['org.ohif.default', 'org.ohif.cornerstone', 'org.ohif.measurement-tracking'],
sopClassHandlers: ['org.ohif.default.sopClassHandlerModule.stack'],
};
}
window.longitudinalMode = mode({});

View File

@ -58,8 +58,10 @@
"@ohif/extension-dicom-microscopy": "^0.50.6",
"@ohif/extension-dicom-pdf": "^1.0.1",
"@ohif/extension-lesion-tracker": "^0.2.0",
"@ohif/extension-measurement-tracking": "^0.0.1",
"@ohif/extension-vtk": "^1.5.6",
"@ohif/i18n": "^0.52.8",
"@ohif/mode-longitudinal": "^0.0.1",
"@ohif/ui": "^1.4.4",
"@tanem/react-nprogress": "^1.1.25",
"@types/react": "^16.0.0",

View File

@ -19,6 +19,7 @@ import appInit from './appInit.js';
// TODO: Temporarily for testing
import '@ohif/mode-example';
import '@ohif/mode-longitudinal';
/**
* ENV Variable to determine routing behavior

View File

@ -66,6 +66,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
// TODO: Remove this
if (!appConfig.modes.length) {
appConfig.modes.push(window.exampleMode);
appConfig.modes.push(window.longitudinalMode);
}
return {

View File

@ -114,7 +114,7 @@ function ViewerViewportGrid(props) {
};
// const ViewportPanes = React.useMemo(getViewportPanes, [
// viewportComp'onents,
// viewportComponents,
// activeViewportIndex,
// viewportGrid,
// ]);

View File

@ -23,11 +23,16 @@ import ReactDOM from 'react-dom';
*/
import OHIFDefaultExtension from '@ohif/extension-default';
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking';
/** Combine our appConfiguration and "baked-in" extensions */
const appProps = {
config: window ? window.config : {},
defaultExtensions: [OHIFDefaultExtension, OHIFCornerstoneExtension],
defaultExtensions: [
OHIFDefaultExtension,
OHIFCornerstoneExtension,
OHIFMeasurementTrackingExtension,
],
};
/** Create App */

View File

@ -868,27 +868,13 @@
pirates "^4.0.0"
source-map-support "^0.5.16"
"@babel/runtime@7.1.2":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.1.2.tgz#81c89935f4647706fc54541145e6b4ecfef4b8e3"
integrity sha512-Y3SCjmhSupzFB6wcv1KmmFucH6gDVnI30WjOcicV10ju0cZjak3Jcs67YLIXBrmZYw1xCrVeJPbycFwrqNyxpg==
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@7.7.6", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6":
"@babel/runtime@7.1.2", "@babel/runtime@7.7.6", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6", "@babel/runtime@^7.7.7", "@babel/runtime@^7.8.4":
version "7.7.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f"
integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.7.7", "@babel/runtime@^7.8.4":
version "7.9.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.6.tgz#a9102eb5cadedf3f31d08a9ecf294af7827ea29f"
integrity sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/standalone@^7.4.5":
version "7.8.6"
resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.8.6.tgz#1364534775c83bf7b7988e4ca98823bef56a0a53"
@ -17296,11 +17282,6 @@ regenerator-runtime@^0.11.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.12.0:
version "0.12.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.2:
version "0.13.3"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"