Support for additional "tracking" mode
This commit is contained in:
parent
fae21bed9c
commit
d557acbb40
8
extensions/measurement-tracking/.webpack/webpack.dev.js
Normal file
8
extensions/measurement-tracking/.webpack/webpack.dev.js
Normal 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 });
|
||||||
|
};
|
||||||
44
extensions/measurement-tracking/.webpack/webpack.prod.js
Normal file
44
extensions/measurement-tracking/.webpack/webpack.prod.js
Normal 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,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
40
extensions/measurement-tracking/package.json
Normal file
40
extensions/measurement-tracking/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
4
extensions/measurement-tracking/src/Panels/index.js
Normal file
4
extensions/measurement-tracking/src/Panels/index.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
|
||||||
|
import PanelMeasurementTableTracking from './PanelMeasurementTableTracking';
|
||||||
|
|
||||||
|
export { PanelMeasurementTableTracking, PanelStudyBrowserTracking };
|
||||||
@ -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;
|
||||||
64
extensions/measurement-tracking/src/getCommandsModule.js
Normal file
64
extensions/measurement-tracking/src/getCommandsModule.js
Normal 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;
|
||||||
38
extensions/measurement-tracking/src/getContextModule.js
Normal file
38
extensions/measurement-tracking/src/getContextModule.js
Normal 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;
|
||||||
41
extensions/measurement-tracking/src/getPanelModule.js
Normal file
41
extensions/measurement-tracking/src/getPanelModule.js
Normal 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;
|
||||||
31
extensions/measurement-tracking/src/getViewportModule.js
Normal file
31
extensions/measurement-tracking/src/getViewportModule.js
Normal 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;
|
||||||
15
extensions/measurement-tracking/src/index.js
Normal file
15
extensions/measurement-tracking/src/index.js
Normal 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,
|
||||||
|
};
|
||||||
8
modes/longitudinal/.webpack/webpack.dev.js
Normal file
8
modes/longitudinal/.webpack/webpack.dev.js
Normal 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 });
|
||||||
|
};
|
||||||
44
modes/longitudinal/.webpack/webpack.prod.js
Normal file
44
modes/longitudinal/.webpack/webpack.prod.js
Normal 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,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
21
modes/longitudinal/LICENSE
Normal file
21
modes/longitudinal/LICENSE
Normal 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.
|
||||||
1
modes/longitudinal/babel.config.js
Normal file
1
modes/longitudinal/babel.config.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = require('../../babel.config.js');
|
||||||
35
modes/longitudinal/package.json
Normal file
35
modes/longitudinal/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
98
modes/longitudinal/src/index.js
Normal file
98
modes/longitudinal/src/index.js
Normal 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({});
|
||||||
@ -58,8 +58,10 @@
|
|||||||
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
"@ohif/extension-dicom-microscopy": "^0.50.6",
|
||||||
"@ohif/extension-dicom-pdf": "^1.0.1",
|
"@ohif/extension-dicom-pdf": "^1.0.1",
|
||||||
"@ohif/extension-lesion-tracker": "^0.2.0",
|
"@ohif/extension-lesion-tracker": "^0.2.0",
|
||||||
|
"@ohif/extension-measurement-tracking": "^0.0.1",
|
||||||
"@ohif/extension-vtk": "^1.5.6",
|
"@ohif/extension-vtk": "^1.5.6",
|
||||||
"@ohif/i18n": "^0.52.8",
|
"@ohif/i18n": "^0.52.8",
|
||||||
|
"@ohif/mode-longitudinal": "^0.0.1",
|
||||||
"@ohif/ui": "^1.4.4",
|
"@ohif/ui": "^1.4.4",
|
||||||
"@tanem/react-nprogress": "^1.1.25",
|
"@tanem/react-nprogress": "^1.1.25",
|
||||||
"@types/react": "^16.0.0",
|
"@types/react": "^16.0.0",
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import appInit from './appInit.js';
|
|||||||
|
|
||||||
// TODO: Temporarily for testing
|
// TODO: Temporarily for testing
|
||||||
import '@ohif/mode-example';
|
import '@ohif/mode-example';
|
||||||
|
import '@ohif/mode-longitudinal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ENV Variable to determine routing behavior
|
* ENV Variable to determine routing behavior
|
||||||
|
|||||||
@ -66,6 +66,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
|||||||
// TODO: Remove this
|
// TODO: Remove this
|
||||||
if (!appConfig.modes.length) {
|
if (!appConfig.modes.length) {
|
||||||
appConfig.modes.push(window.exampleMode);
|
appConfig.modes.push(window.exampleMode);
|
||||||
|
appConfig.modes.push(window.longitudinalMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -114,7 +114,7 @@ function ViewerViewportGrid(props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// const ViewportPanes = React.useMemo(getViewportPanes, [
|
// const ViewportPanes = React.useMemo(getViewportPanes, [
|
||||||
// viewportComp'onents,
|
// viewportComponents,
|
||||||
// activeViewportIndex,
|
// activeViewportIndex,
|
||||||
// viewportGrid,
|
// viewportGrid,
|
||||||
// ]);
|
// ]);
|
||||||
|
|||||||
@ -23,11 +23,16 @@ import ReactDOM from 'react-dom';
|
|||||||
*/
|
*/
|
||||||
import OHIFDefaultExtension from '@ohif/extension-default';
|
import OHIFDefaultExtension from '@ohif/extension-default';
|
||||||
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
|
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
|
||||||
|
import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking';
|
||||||
|
|
||||||
/** Combine our appConfiguration and "baked-in" extensions */
|
/** Combine our appConfiguration and "baked-in" extensions */
|
||||||
const appProps = {
|
const appProps = {
|
||||||
config: window ? window.config : {},
|
config: window ? window.config : {},
|
||||||
defaultExtensions: [OHIFDefaultExtension, OHIFCornerstoneExtension],
|
defaultExtensions: [
|
||||||
|
OHIFDefaultExtension,
|
||||||
|
OHIFCornerstoneExtension,
|
||||||
|
OHIFMeasurementTrackingExtension,
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Create App */
|
/** Create App */
|
||||||
|
|||||||
21
yarn.lock
21
yarn.lock
@ -868,27 +868,13 @@
|
|||||||
pirates "^4.0.0"
|
pirates "^4.0.0"
|
||||||
source-map-support "^0.5.16"
|
source-map-support "^0.5.16"
|
||||||
|
|
||||||
"@babel/runtime@7.1.2":
|
"@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.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":
|
|
||||||
version "7.7.6"
|
version "7.7.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f"
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.7.6.tgz#d18c511121aff1b4f2cd1d452f1bac9601dd830f"
|
||||||
integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw==
|
integrity sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.2"
|
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":
|
"@babel/standalone@^7.4.5":
|
||||||
version "7.8.6"
|
version "7.8.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.8.6.tgz#1364534775c83bf7b7988e4ca98823bef56a0a53"
|
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"
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
|
||||||
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
|
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:
|
regenerator-runtime@^0.13.1, regenerator-runtime@^0.13.2:
|
||||||
version "0.13.3"
|
version "0.13.3"
|
||||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
|
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user