{
+ 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;
+}
diff --git a/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js b/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js
new file mode 100644
index 000000000..baf46c84a
--- /dev/null
+++ b/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/getImageSrcFromImageId.js
@@ -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;
diff --git a/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/getStudiesForPatientByStudyInstanceUID.js b/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/getStudiesForPatientByStudyInstanceUID.js
new file mode 100644
index 000000000..9327383ee
--- /dev/null
+++ b/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/getStudiesForPatientByStudyInstanceUID.js
@@ -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;
diff --git a/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/index.jsx b/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/index.jsx
new file mode 100644
index 000000000..c3bb27d01
--- /dev/null
+++ b/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/index.jsx
@@ -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 (
+
+ );
+}
+
+/**
+ * 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;
diff --git a/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/requestDisplaySetCreationForStudy.js b/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/requestDisplaySetCreationForStudy.js
new file mode 100644
index 000000000..2cf412f3e
--- /dev/null
+++ b/extensions/measurement-tracking/src/Panels/PanelStudyBrowserTracking/requestDisplaySetCreationForStudy.js
@@ -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;
diff --git a/extensions/measurement-tracking/src/Panels/index.js b/extensions/measurement-tracking/src/Panels/index.js
new file mode 100644
index 000000000..4db70ae02
--- /dev/null
+++ b/extensions/measurement-tracking/src/Panels/index.js
@@ -0,0 +1,4 @@
+import PanelStudyBrowserTracking from './PanelStudyBrowserTracking';
+import PanelMeasurementTableTracking from './PanelMeasurementTableTracking';
+
+export { PanelMeasurementTableTracking, PanelStudyBrowserTracking };
diff --git a/extensions/measurement-tracking/src/Viewports/OHIFCornerstoneViewport.js b/extensions/measurement-tracking/src/Viewports/OHIFCornerstoneViewport.js
new file mode 100644
index 000000000..049d93c15
--- /dev/null
+++ b/extensions/measurement-tracking/src/Viewports/OHIFCornerstoneViewport.js
@@ -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 (
+ <>
+
+ {childrenWithProps}
+ >
+ );
+ }
+}
+
+export default OHIFCornerstoneViewport;
diff --git a/extensions/measurement-tracking/src/getCommandsModule.js b/extensions/measurement-tracking/src/getCommandsModule.js
new file mode 100644
index 000000000..5a500b030
--- /dev/null
+++ b/extensions/measurement-tracking/src/getCommandsModule.js
@@ -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 {
+ dispatch({ type: '', payload: {
+ numCols: 3,
+ numRows: 3,
+ activeViewportIndex: 0,
+ viewports: [],
+ }})
+ }}
+ style={{ color: 'white' }}
+ >
+ Hello World!
+
+}
+
+export default getCommandsModule;
diff --git a/extensions/measurement-tracking/src/getContextModule.js b/extensions/measurement-tracking/src/getContextModule.js
new file mode 100644
index 000000000..ddca1fcfd
--- /dev/null
+++ b/extensions/measurement-tracking/src/getContextModule.js
@@ -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 (
+
+ {children}
+
+ );
+}
+
+function getContextModule() {
+ return [
+ {
+ name: 'HelloWorldContext',
+ context: HelloWorldContext,
+ provider: HelloWorldContextProvider,
+ },
+ ];
+}
+
+export { HelloWorldContext };
+
+export default getContextModule;
diff --git a/extensions/measurement-tracking/src/getPanelModule.js b/extensions/measurement-tracking/src/getPanelModule.js
new file mode 100644
index 000000000..b1ef8911d
--- /dev/null
+++ b/extensions/measurement-tracking/src/getPanelModule.js
@@ -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;
diff --git a/extensions/measurement-tracking/src/getViewportModule.js b/extensions/measurement-tracking/src/getViewportModule.js
new file mode 100644
index 000000000..d9ac86081
--- /dev/null
+++ b/extensions/measurement-tracking/src/getViewportModule.js
@@ -0,0 +1,31 @@
+import React from 'react';
+
+const Component = React.lazy(() => {
+ return import('./Viewports/OHIFCornerstoneViewport');
+});
+
+const OHIFCornerstoneViewport = props => {
+ return (
+ Loading...}>
+
+
+ );
+};
+
+function getViewportModule({ commandsManager }) {
+ const ExtendedOHIFCornerstoneViewport = props => {
+ const onNewImageHandler = jumpData => {
+ alert('here we are!')
+ commandsManager.runCommand('jumpToImage', jumpData);
+ };
+ return (
+
+ );
+ };
+
+ return [
+ { name: 'cornerstone-tracked', component: ExtendedOHIFCornerstoneViewport },
+ ];
+};
+
+export default getViewportModule;
diff --git a/extensions/measurement-tracking/src/index.js b/extensions/measurement-tracking/src/index.js
new file mode 100644
index 000000000..7aa5cc79b
--- /dev/null
+++ b/extensions/measurement-tracking/src/index.js
@@ -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,
+};
diff --git a/modes/longitudinal/.webpack/webpack.dev.js b/modes/longitudinal/.webpack/webpack.dev.js
new file mode 100644
index 000000000..1ae308448
--- /dev/null
+++ b/modes/longitudinal/.webpack/webpack.dev.js
@@ -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 });
+};
diff --git a/modes/longitudinal/.webpack/webpack.prod.js b/modes/longitudinal/.webpack/webpack.prod.js
new file mode 100644
index 000000000..ca612cd37
--- /dev/null
+++ b/modes/longitudinal/.webpack/webpack.prod.js
@@ -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,
+ }),
+ ],
+ });
+};
diff --git a/modes/longitudinal/LICENSE b/modes/longitudinal/LICENSE
new file mode 100644
index 000000000..19e20dd35
--- /dev/null
+++ b/modes/longitudinal/LICENSE
@@ -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.
diff --git a/modes/longitudinal/babel.config.js b/modes/longitudinal/babel.config.js
new file mode 100644
index 000000000..325ca2a8e
--- /dev/null
+++ b/modes/longitudinal/babel.config.js
@@ -0,0 +1 @@
+module.exports = require('../../babel.config.js');
diff --git a/modes/longitudinal/package.json b/modes/longitudinal/package.json
new file mode 100644
index 000000000..03ed71133
--- /dev/null
+++ b/modes/longitudinal/package.json
@@ -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"
+ }
+}
diff --git a/modes/longitudinal/src/index.js b/modes/longitudinal/src/index.js
new file mode 100644
index 000000000..a929112bf
--- /dev/null
+++ b/modes/longitudinal/src/index.js
@@ -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({});
diff --git a/platform/viewer/package.json b/platform/viewer/package.json
index 6d4b0a545..4c22092c5 100644
--- a/platform/viewer/package.json
+++ b/platform/viewer/package.json
@@ -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",
diff --git a/platform/viewer/src/App.jsx b/platform/viewer/src/App.jsx
index 9eda2e092..22e3c23c0 100644
--- a/platform/viewer/src/App.jsx
+++ b/platform/viewer/src/App.jsx
@@ -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
diff --git a/platform/viewer/src/appInit.js b/platform/viewer/src/appInit.js
index 62fcf2c51..4d236ee73 100644
--- a/platform/viewer/src/appInit.js
+++ b/platform/viewer/src/appInit.js
@@ -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 {
diff --git a/platform/viewer/src/components/ViewportGrid.jsx b/platform/viewer/src/components/ViewportGrid.jsx
index 7ceeeddd5..5fb08add1 100644
--- a/platform/viewer/src/components/ViewportGrid.jsx
+++ b/platform/viewer/src/components/ViewportGrid.jsx
@@ -114,7 +114,7 @@ function ViewerViewportGrid(props) {
};
// const ViewportPanes = React.useMemo(getViewportPanes, [
- // viewportComp'onents,
+ // viewportComponents,
// activeViewportIndex,
// viewportGrid,
// ]);
diff --git a/platform/viewer/src/index.js b/platform/viewer/src/index.js
index b3ad3934c..c742a5c95 100644
--- a/platform/viewer/src/index.js
+++ b/platform/viewer/src/index.js
@@ -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 */
diff --git a/yarn.lock b/yarn.lock
index bcd4cdce0..df2fa9e3e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"