wip on react transition

This commit is contained in:
Erik Ziegler 2018-11-30 11:40:01 -05:00
parent 5a4deedda8
commit 7425c2beb8
36 changed files with 1676 additions and 251 deletions

View File

@ -51,3 +51,4 @@ cultofcoders:persistent-session
shell-server@0.3.1
underscore
meteortesting:mocha
react-template-helper

View File

@ -104,6 +104,7 @@ practicalmeteor:mocha-core@1.0.1
promise@0.11.1
raix:eventemitter@0.1.3
random@1.1.0
react-template-helper@0.2.11
reactive-dict@1.2.0
reactive-var@1.0.11
reload@1.2.0
@ -123,6 +124,7 @@ templating@1.3.2
templating-compiler@1.3.3
templating-runtime@1.3.2
templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.2
tracker@1.2.0
ui@1.0.13
underscore@1.0.10

View File

@ -4,7 +4,7 @@
{{> studyBrowser (clone this)}}
</div>
<div class="mainContent {{#if leftSidebarOpen}}sidebar-left-open{{/if}} {{#if rightSidebarOpen}}sidebar-right-open{{/if}}">
{{> viewerMain (clone this)}}
{{> React component=ViewerMain studies=this.studies}}
</div>
<div class="sidebarMenu sidebar-right {{#if rightSidebarOpen}}sidebar-open{{/if}}">
{{> measurementLightTable (clone this)}}

View File

@ -1,3 +1,5 @@
import { Viewerbase } from 'meteor/ohif:viewerbase';
Template.flexboxLayout.events({
'transitionend .sidebarMenu'(event) {
if (!event.target.classList.contains('sidebarMenu')) {
@ -15,5 +17,9 @@ Template.flexboxLayout.helpers({
rightSidebarOpen() {
return Template.instance().data.state.get('rightSidebar');
},
ViewerMain() {
return Viewerbase.components.viewer.ViewerMain;
}
});

View File

@ -155,6 +155,33 @@
"path-is-absolute": "^1.0.0"
}
},
"history": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/history/-/history-4.7.2.tgz",
"integrity": "sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==",
"requires": {
"invariant": "^2.2.1",
"loose-envify": "^1.2.0",
"resolve-pathname": "^2.2.0",
"value-equal": "^0.4.0",
"warning": "^3.0.0"
},
"dependencies": {
"warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz",
"integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=",
"requires": {
"loose-envify": "^1.0.0"
}
}
}
},
"hoist-non-react-statics": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz",
"integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw=="
},
"https-proxy-agent": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz",
@ -181,17 +208,48 @@
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"dev": true
},
"invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
},
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
},
"lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
},
"loglevel": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.6.1.tgz",
"integrity": "sha1-4PyVEztu8nbNyIh82vJKpvFW+Po="
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
},
"mime": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz",
@ -228,6 +286,11 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -243,6 +306,21 @@
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"path-to-regexp": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
"requires": {
"isarray": "0.0.1"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
}
}
},
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@ -261,6 +339,15 @@
"integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=",
"dev": true
},
"prop-types": {
"version": "15.6.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz",
"integrity": "sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ==",
"requires": {
"loose-envify": "^1.3.1",
"object-assign": "^4.1.1"
}
},
"proxy-from-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
@ -283,6 +370,55 @@
"ws": "^5.1.1"
}
},
"react": {
"version": "16.6.3",
"resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz",
"integrity": "sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.11.2"
}
},
"react-dom": {
"version": "16.6.3",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.6.3.tgz",
"integrity": "sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"scheduler": "^0.11.2"
}
},
"react-router": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz",
"integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==",
"requires": {
"history": "^4.7.2",
"hoist-non-react-statics": "^2.5.0",
"invariant": "^2.2.4",
"loose-envify": "^1.3.1",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.1",
"warning": "^4.0.1"
}
},
"react-router-dom": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz",
"integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==",
"requires": {
"history": "^4.7.2",
"invariant": "^2.2.4",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.1",
"react-router": "^4.3.1",
"warning": "^4.0.1"
}
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
@ -298,11 +434,25 @@
"util-deprecate": "~1.0.1"
}
},
"redux": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz",
"integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==",
"requires": {
"loose-envify": "^1.4.0",
"symbol-observable": "^1.2.0"
}
},
"regenerator-runtime": {
"version": "0.11.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
},
"resolve-pathname": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.2.0.tgz",
"integrity": "sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg=="
},
"rimraf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
@ -318,6 +468,15 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true
},
"scheduler": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.2.tgz",
"integrity": "sha512-+WCP3s3wOaW4S7C1tl3TEXp4l9lJn0ZK8G3W3WKRWmw77Z2cIFUW2MiNTMHn5sCjxN+t7N43HAOOgMjyAg5hlg==",
"requires": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
@ -327,6 +486,11 @@
"safe-buffer": "~5.1.0"
}
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@ -339,6 +503,19 @@
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"dev": true
},
"value-equal": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.4.0.tgz",
"integrity": "sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw=="
},
"warning": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
"integrity": "sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@ -5,7 +5,14 @@
"main": "index.js",
"dependencies": {
"@babel/runtime": "7.0.0-beta.51",
"loglevel": "^1.6.1"
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"loglevel": "^1.6.1",
"prop-types": "^15.6.2",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-router-dom": "^4.3.1",
"redux": "^4.0.1"
},
"devDependencies": {
"puppeteer": "^1.5.0"

View File

@ -0,0 +1,6 @@
import viewer from './viewer/index.js';
export {
viewer
};

View File

@ -0,0 +1,4 @@
.CornerstoneViewport {
width: 100%;
height: 100%;
}

View File

@ -0,0 +1,645 @@
import { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
//import * as cornerstone from 'cornerstone-core';
//import * as cornerstoneTools from 'cornerstone-tools';
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
//import './lib/initCornerstone.js';
import ImageScrollbar from './ImageScrollbar.js';
import ViewportOverlay from './ViewportOverlay.js';
import ToolContextMenu from './ToolContextMenu.js';
import LoadingIndicator from './LoadingIndicator/LoadingIndicator.js';
import './CornerstoneViewport.css';
import cloneDeep from 'lodash.clonedeep';
import debounce from 'lodash.debounce';
const EVENT_RESIZE = 'resize';
const loadIndicatorDelay = 45;
const { loadHandlerManager } = cornerstoneTools;
function setToolsPassive(tools) {
tools.forEach(tool => {
cornerstoneTools.setToolPassive(tool);
});
}
function initializeTools(tools) {
Array.from(tools).forEach(tool => {
const apiTool = cornerstoneTools[`${tool.name}Tool`];
if (apiTool) {
cornerstoneTools.addTool(apiTool, tool.configuration);
} else {
throw new Error(`Tool not found: ${tool.name}Tool`);
}
});
}
const scrollToIndex = cornerstoneTools.import('util/scrollToIndex');
class CornerstoneViewport extends Component {
constructor(props) {
super(props);
const stack = props.viewportData.stack;
// TODO: Allow viewport as a prop
this.state = {
stack,
imageId: stack.imageIds[0],
viewportHeight: '100%',
isLoading: true,
imageScrollbarValue: 0,
numImagesLoaded: 0
};
this.displayScrollbar = stack.imageIds.length > 1;
this.state.viewport = cornerstone.getDefaultViewport(null, undefined);
this.onImageRendered = this.onImageRendered.bind(this);
this.onNewImage = this.onNewImage.bind(this);
this.onWindowResize = this.onWindowResize.bind(this);
this.onImageLoaded = this.onImageLoaded.bind(this);
this.onStackScroll = this.onStackScroll.bind(this);
this.startLoadingHandler = this.startLoadingHandler.bind(this);
this.doneLoadingHandler = this.doneLoadingHandler.bind(this);
this.onMouseClick = this.onMouseClick.bind(this);
this.onTouchPress = this.onTouchPress.bind(this);
this.onMeasurementAddedOrRemoved = this.onMeasurementAddedOrRemoved.bind(
this
);
this.onCloseToolContextMenu = this.onCloseToolContextMenu.bind(this);
this.imageSliderOnInputCallback = this.imageSliderOnInputCallback.bind(
this
);
this.loadHandlerTimeout = 25;
loadHandlerManager.setStartLoadHandler(this.startLoadingHandler);
loadHandlerManager.setEndLoadHandler(this.doneLoadingHandler);
this.debouncedResize = _.debounce(() => {
cornerstone.resize(this.element, true);
this.setState({
viewportHeight: `${this.element.clientHeight - 20}px`
});
}, 300);
this.slideTimeoutTime = 25;
this.slideTimeout = null;
}
render() {
const isLoading =
this.state.isLoading ||
this.state.numImagesLoaded / this.state.stack.imageIds.length < 0.1;
return (
<>
<ToolContextMenu
toolContextMenuData={this.state.toolContextMenuData}
onClose={this.onCloseToolContextMenu}
/>
<div
className="CornerstoneViewport viewportElement"
onContextMenu={this.onContextMenu}
ref={input => {
this.element = input;
}}
>
{isLoading ? <LoadingIndicator /> : ''}
<canvas className="cornerstone-canvas" />
<ViewportOverlay
stack={this.state.stack}
viewport={this.state.viewport}
imageId={this.state.imageId}
numImagesLoaded={this.state.numImagesLoaded}
/>
</div>
{this.displayScrollbar && (
<ImageScrollbar
onInputCallback={this.imageSliderOnInputCallback}
max={this.state.stack.imageIds.length - 1}
value={this.state.imageScrollbarValue}
height={this.state.viewportHeight}
/>
)}
{/*this.state.bidirectionalAddLabelShow && (
<Labelling
measurementData={this.bidirectional.measurementData}
eventData={this.bidirectional.eventData}
labellingDoneCallback={this.bidirectional.labellingDoneCallback}
skipButton={this.bidirectional.skipButton}
editDescription={this.bidirectional.editDescription}
/>
)*/}
</>
);
}
bidirectionalToolLabellingCallback = (
measurementData,
eventData,
doneCallback,
options = {}
) => {
const labellingDoneCallback = () => {
this.hideExtraButtons();
return doneCallback();
};
this.bidirectional = {
measurementData,
eventData,
labellingDoneCallback,
skipButton: options.skipButton,
editDescription: options.editDescription
};
this.setState({
bidirectionalAddLabelShow: true
});
};
onContextMenu(event) {
// Preventing the default behaviour for right-click is essential to
// allow right-click tools to work.
event.preventDefault();
}
onWindowResize() {
this.debouncedResize();
}
onImageRendered() {
const viewport = cornerstone.getViewport(this.element);
this.setState({
viewport
});
}
onNewImage() {
const image = cornerstone.getImage(this.element);
this.setState({
imageId: image.imageId
});
}
componentDidMount() {
const element = this.element;
// Enable the DOM Element for use with Cornerstone
cornerstone.enable(element);
cornerstone.events.addEventListener(
cornerstone.EVENTS.IMAGE_LOADED,
this.onImageLoaded
);
// Load the first image in the stack
cornerstone.loadAndCacheImage(this.state.imageId).then(image => {
try {
cornerstone.getEnabledElement(element);
} catch (error) {
// Handle cases where the user ends the session before the image is displayed.
console.error(error);
return;
}
// Set Soft Tissue preset for all images by default
const viewport = cornerstone.getDefaultViewportForImage(element, image);
viewport.voi = {
windowWidth: 400,
windowCenter: 40
};
// Display the first image
cornerstone.displayImage(element, image, viewport);
// Clear any previous tool state
cornerstoneTools.clearToolState(this.element, 'stack');
// Disable stack prefetch in case there are still queued requests
cornerstoneTools.stackPrefetch.disable(this.element);
// Add the stack tool state to the enabled element
const stack = this.state.stack;
cornerstoneTools.addStackStateManager(element, ['stack']);
cornerstoneTools.addToolState(element, 'stack', stack);
cornerstoneTools.stackPrefetch.enable(this.element);
const tools = [
{
name: 'Bidirectional',
configuration: {
getMeasurementLocationCallback: this
.bidirectionalToolLabellingCallback,
shadow: true,
drawHandlesOnHover: true
}
},
{ name: 'Wwwc' },
{
name: 'Zoom',
configuration: {
minScale: 0.3,
maxScale: 25,
preventZoomOutsideImage: true
}
},
{ name: 'Pan' },
{ name: 'StackScroll' },
{ name: 'PanMultiTouch' },
{ name: 'ZoomTouchPinch' },
{ name: 'StackScrollMouseWheel' },
{ name: 'StackScrollMultiTouch' }
];
initializeTools(tools);
this.setActiveTool(this.props.activeTool);
/* For touch devices, by default we activate:
- Pinch to zoom
- Two-finger Pan
- Three (or more) finger Stack Scroll
*/
cornerstoneTools.setToolActive('PanMultiTouch', {
mouseButtonMask: 0,
isTouchActive: true
});
cornerstoneTools.setToolActive('ZoomTouchPinch', {
mouseButtonMask: 0,
isTouchActive: true
});
cornerstoneTools.setToolActive('StackScrollMultiTouch', {
mouseButtonMask: 0,
isTouchActive: true
});
cornerstoneTools.stackPrefetch.setConfiguration({
maxImagesToPrefetch: Infinity,
preserveExistingPool: false,
maxSimultaneousRequests: 6
});
/* For mouse devices, by default we turn on:
- Stack scrolling by mouse wheel
- Stack scrolling by keyboard up / down arrow keys
- Pan with middle click
- Zoom with right click
*/
cornerstoneTools.setToolActive('StackScrollMouseWheel', {
mouseButtonMask: 0,
isTouchActive: true
});
element.addEventListener(
cornerstone.EVENTS.IMAGE_RENDERED,
this.onImageRendered
);
element.addEventListener(cornerstone.EVENTS.NEW_IMAGE, this.onNewImage);
element.addEventListener(
cornerstoneTools.EVENTS.STACK_SCROLL,
this.onStackScroll
);
element.addEventListener(
cornerstoneTools.EVENTS.MEASUREMENT_ADDED,
this.onMeasurementAddedOrRemoved
);
element.addEventListener(
cornerstoneTools.EVENTS.MEASUREMENT_REMOVED,
this.onMeasurementAddedOrRemoved
);
element.addEventListener(
cornerstoneTools.EVENTS.MOUSE_CLICK,
this.onMouseClick
);
element.addEventListener(
cornerstoneTools.EVENTS.TOUCH_PRESS,
this.onTouchPress
);
window.addEventListener(EVENT_RESIZE, this.onWindowResize);
this.setState({
viewportHeight: `${this.element.clientHeight - 20}px`
});
this.doneLoadingHandler();
});
}
componentWillUnmount() {
const element = this.element;
element.removeEventListener(
cornerstone.EVENTS.IMAGE_RENDERED,
this.onImageRendered
);
element.removeEventListener(cornerstone.EVENTS.NEW_IMAGE, this.onNewImage);
element.removeEventListener(
cornerstoneTools.EVENTS.STACK_SCROLL,
this.onStackScroll
);
element.removeEventListener(
cornerstoneTools.EVENTS.MEASUREMENT_ADDED,
this.onMeasurementAddedOrRemoved
);
element.removeEventListener(
cornerstoneTools.EVENTS.MEASUREMENT_REMOVED,
this.onMeasurementAddedOrRemoved
);
element.removeEventListener(
cornerstoneTools.EVENTS.MOUSE_CLICK,
this.onMouseClick
);
element.removeEventListener(
cornerstoneTools.EVENTS.TOUCH_PRESS,
this.onTouchPress
);
window.removeEventListener(EVENT_RESIZE, this.onWindowResize);
cornerstone.disable(element);
cornerstone.events.removeEventListener(
cornerstone.EVENTS.IMAGE_LOADED,
this.onImageLoaded
);
}
componentDidUpdate(prevProps) {
// TODO: Add a real object shallow comparison here?
if (
this.state.stack.imageIds[0] !== this.props.viewportData.stack.imageIds[0]
) {
this.setState({
stack: this.props.viewportData.stack
});
const stackData = cornerstoneTools.getToolState(this.element, 'stack');
let currentStack = stackData && stackData.data[0];
if (!currentStack) {
currentStack = {
currentImageIdIndex: this.state.stack.currentImageIdIndex,
imageIds: this.state.stack.imageIds
};
cornerstoneTools.addStackStateManager(this.element, ['stack']);
cornerstoneTools.addToolState(this.element, 'stack', currentStack);
} else {
// TODO: we should make something like setToolState by an ID
currentStack.currentImageIdIndex = this.state.stack.currentImageIdIndex;
currentStack.imageIds = this.state.stack.imageIds;
}
const imageId = currentStack.imageIds[currentStack.currentImageIdIndex];
cornerstone.loadAndCacheImage(imageId).then(image => {
try {
cornerstone.getEnabledElement(this.element);
} catch (error) {
// Handle cases where the user ends the session before the image is displayed.
console.error(error);
return;
}
cornerstone.displayImage(this.element, image);
cornerstoneTools.stackPrefetch.disable(this.element);
cornerstoneTools.stackPrefetch.enable(this.element);
});
}
if (this.props.activeTool !== prevProps.activeTool) {
this.setActiveTool(this.props.activeTool);
// TODO: Why do we need to do this in v3?
cornerstoneTools.setToolActive('StackScrollMouseWheel', {
mouseButtonMask: 0,
isTouchActive: true
});
}
if (this.props.currentLesion !== prevProps.currentLesion) {
const currentToolData = this.props.toolData[this.props.currentLesion - 1];
if (currentToolData) {
const { imageId } = currentToolData;
const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
const toolData = toolState[imageId][currentToolData.toolType].data;
toolData.forEach(data => {
if (data._id === currentToolData._id) {
data.active = true;
} else {
data.active = false;
}
});
cornerstoneTools.globalImageIdSpecificToolStateManager.restoreToolState(
toolState
);
if (this.state.imageId === imageId) {
cornerstone.setViewport(this.element, currentToolData.viewport);
cornerstone.updateImage(this.element);
} else {
cornerstone.loadAndCacheImage(imageId).then(image => {
try {
cornerstone.getEnabledElement(this.element);
} catch (error) {
// Handle cases where the user ends the session before the image is displayed.
console.error(error);
return;
}
cornerstone.displayImage(
this.element,
image,
currentToolData.viewport
);
this.setState({
imageId
});
});
}
} else {
cornerstone.updateImage(this.element);
}
}
}
setActiveTool = activeTool => {
const leftMouseTools = ['Bidirectional', 'Wwwc', 'StackScroll'];
setToolsPassive(leftMouseTools);
// pan is the default tool for middle mouse button
const isPanToolActive = activeTool === 'Pan';
const panOptions = {
mouseButtonMask: isPanToolActive ? [1, 4] : [4],
isTouchActive: isPanToolActive
};
cornerstoneTools.setToolActive('Pan', panOptions);
// zoom is the default tool for right mouse button
const isZoomToolActive = activeTool === 'Zoom';
const zoomOptions = {
mouseButtonMask: isZoomToolActive ? [1, 2] : [2],
isTouchActive: isZoomToolActive
};
cornerstoneTools.setToolActive('Zoom', zoomOptions);
cornerstoneTools.setToolActive(activeTool, {
mouseButtonMask: 1,
isTouchActive: true
});
};
onStackScroll(event) {
const element = event.currentTarget;
const stackData = cornerstoneTools.getToolState(element, 'stack');
const stack = stackData.data[0];
this.hideExtraButtons();
this.setState({
stack,
imageScrollbarValue: stack.currentImageIdIndex
});
}
onImageLoaded(event) {
this.setState({
numImagesLoaded: this.state.numImagesLoaded + 1
});
}
startLoadingHandler() {
//console.log('startLoadingHandler');
clearTimeout(this.loadHandlerTimeout);
this.loadHandlerTimeout = setTimeout(() => {
this.setState({
isLoading: true
});
}, loadIndicatorDelay);
}
doneLoadingHandler() {
clearTimeout(this.loadHandlerTimeout);
this.setState({
isLoading: false
});
}
onMeasurementAddedOrRemoved(event) {
console.log('onMeasurementAddedOrRemoved');
/*const { toolType, measurementData } = event.detail;
// TODO: Pass in as prop?
const toolsOfInterest = ['Bidirectional'];
this.hideExtraButtons();
if (toolsOfInterest.includes(toolType)) {
const image = cornerstone.getImage(this.element);
const viewport = cornerstone.getViewport(this.element);
const type = {
cornerstonetoolsmeasurementadded: 'added',
cornerstonetoolsmeasurementremoved: 'removed'
};
const action = type[event.type];
if (action === 'added') {
measurementData._id = guid();
measurementData.viewport = cloneDeep(viewport);
}
this.props.measurementsAddedOrRemoved(
action,
image.imageId,
toolType,
measurementData
);
}*/
}
onMouseClick(event) {
if (event.detail.event.which === 3) {
this.setState({
toolContextMenuData: {
eventData: event.detail,
isTouchEvent: false
}
});
}
}
onTouchPress(event) {
this.setState({
toolContextMenuData: {
eventData: event.detail,
isTouchEvent: true
}
});
}
onCloseToolContextMenu() {
this.setState({
toolContextMenuData: null
});
}
imageSliderOnInputCallback(value) {
this.setState({
imageScrollbarValue: value
});
// Note that we throttle requests to prevent the
// user's ultrafast scrolling from firing requests too quickly.
//clearTimeout(this.slideTimeout);
//this.slideTimeout = setTimeout(() => {
scrollToIndex(this.element, value);
//}, this.slideTimeoutTime);
}
hideExtraButtons = () => {
if (this.state.bidirectionalAddLabelShow === true) {
this.setState({
bidirectionalAddLabelShow: false
});
}
this.setState({
toolContextMenuData: null
});
};
}
CornerstoneViewport.propTypes = {
measurementsAddedOrRemoved: PropTypes.func.isRequired,
measurementsChanged: PropTypes.func.isRequired,
activeTool: PropTypes.string.isRequired,
viewportData: PropTypes.object.isRequired
};
export default CornerstoneViewport;

View File

@ -0,0 +1,115 @@
import { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { OHIF } from 'meteor/ohif:core';
import CornerstoneViewport from '../CornerstoneViewport.js';
import './GridLayout.styl';
const TOP_CLASS = 'top';
const BOTTOM_CLASS = 'bottom';
const MIDDLE_CLASS = 'middle';
class GridLayout extends Component {
constructor(props) {
super(props);
this.getClass = this.getClass.bind(this);
}
// Get class for each viewport, so each app
// using ohif-viewerbase can style on their own
getClass(index) {
const { rows, columns } = this.props;
if (rows === 1) {
return `${TOP_CLASS} ${BOTTOM_CLASS}`;
}
const actualRow = Math.floor(index / columns);
if ( actualRow === 0 ) {
return TOP_CLASS;
}
if ( actualRow + 1 === rows ) {
return BOTTOM_CLASS;
}
return MIDDLE_CLASS;
}
getActiveClass(index) {
if (this.props.activeViewport === index) {
return 'active';
};
return ''
}
render() {
// Get the height percentage for each viewport
const rows = this.props.rows || 1;
const height = 100 / rows;
// Get the width percentage for each viewport
const columns = this.props.columns || 1;
const width = 100 / columns;
let viewportData = this.props.viewportData;
const numViewports = rows * columns;
const numViewportsWithData = viewportData.length;
// Check if the viewportData length is different from the given
if (numViewportsWithData < numViewports) {
// Add the missing viewports
const difference = numViewports - numViewportsWithData;
for (let i = 0; i < difference; i++) {
viewportData.push({
viewportIndex: numViewportsWithData + i + 1,
rows,
columns
});
}
} else if (numViewportsWithData > numViewports) {
// Remove the additional viewports
viewportData = viewportData.slice(0, numViewports);
}
const viewports = viewportData.map((viewport, index) => {
const className = `viewportContainer ${this.getClass(index)} ${getActiveClass(index)}`;
const styles = {
height: `${height}%`,
width: `${width}%`,
};
const cornerstoneViewport = (viewport) => (
<div className="removable">
<CornerstoneViewport/>
</div>
);
const pluginViewport = (viewport) => (
<div className={`viewport-plugin-${viewport.plugin}`} style="height:100%; width: 100%">
</div>
);
return (
<div className={className} style={styles}>
{viewport.plugin === 'cornerstone' ?
cornerstoneViewport(viewport) :
pluginViewport(viewport)
}
{/*{>seriesQuickSwitch (clone this viewport=viewport viewportIndex=@index)}*/}
</div>
)
})
return (
<div id='imageViewerViewports'>
{ viewports }
</div>
)
}
}
export default GridLayout;

View File

@ -0,0 +1,49 @@
@value globals: "../GlobalStyles.css";
@value activeColor, imageSliderColor from globals;
.scroll {
position: absolute;
top: 0;
right: 0;
width: 15px;
height: 100%;
z-index: 2;
}
.scroll .scroll-holder {
position: relative;
height: 100%;
}
.scroll > .scroll-holder > .imageSlider {
transform: rotate(90deg);
transform-origin: top left;
position: absolute;
width: 100%; /* Note: This width does not have an effect. */
margin-left: 10px;
-webkit-appearance: none;
background-color: rgba(0, 0, 0, 0);
outline: none;
}
input.imageSlider:focus {
outline: none;
}
input.imageSlider::-moz-focus-outer {
border: none;
}
input.imageSlider::-webkit-slider-thumb {
cursor: -webkit-grab;
background: imageSliderColor;
-webkit-appearance: none;
width: 45px;
height: 20px;
border-radius: 10px;
}
input.imageSlider::-webkit-slider-thumb:active {
background: activeColor;
cursor: -webkit-grabbing;
}

View File

@ -0,0 +1,47 @@
import { Component } from 'react';
import React from 'react';
import './ImageScrollbar.css';
import PropTypes from 'prop-types';
class ImageScrollbar extends Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
render() {
this.style = {
width: `${this.props.height}`
};
return (
<div className="scroll">
<div className="scroll-holder">
<input
className="imageSlider"
style={this.style}
type="range"
min="0"
max={this.props.max}
step="1"
value={this.props.value}
onChange={this.onChange}
/>
</div>
</div>
);
}
onChange(event) {
const intValue = parseInt(event.target.value, 10);
this.props.onInputCallback(intValue);
}
}
ImageScrollbar.propTypes = {
value: PropTypes.number.isRequired,
onInputCallback: PropTypes.func.isRequired
};
export default ImageScrollbar;

View File

@ -1,11 +1,15 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { $ } from 'meteor/jquery';
import { OHIF } from 'meteor/ohif:core';
import { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import './LoadingIndicator.styl';
Meteor.startup(() => {
// This checking is necessary because cornerstoneTools may not have some tools available.
// Example: when an app defines its own cornerstone's lib versions, so it
// Example: when an app defines its own cornerstone's lib versions, so it
// uses only ohif-viewerbase and not ohif-cornerstone and those libs are added later.
if (cornerstoneTools.loadHandlerManager) {
cornerstoneTools.loadHandlerManager.setStartLoadHandler(startLoadingHandler);
@ -69,11 +73,38 @@ const errorLoadingHandler = (element, imageId, error, source) => {
}
};
Template.loadingIndicator.helpers({
'percentComplete'() {
const percentComplete = Session.get('CornerstoneLoadProgress' + this.viewportIndex);
if (percentComplete && percentComplete !== 100) {
return `${percentComplete}%`;
}
class LoadingIndicator extends Component {
constructor(props) {
super(props);
}
});
render() {
// TODO[react]: Pass this in as a prop reactively
//const percentComplete = Session.get('CornerstoneLoadProgress' + this.viewportIndex);
let percComplete;
if (this.props.percentComplete && this.props.percentComplete !== 100) {
percComplete = `${this.props.percentComplete}%`;
}
return (<>
<div className="imageViewerLoadingIndicator loadingIndicator">
<div className="indicatorContents">
<p>Loading... <i className="fa fa-spin fa-circle-o-notch fa-fw"></i> {{percComplete}}</p>
</div>
</div>
<div className="imageViewerErrorLoadingIndicator loadingIndicator">
<div className="indicatorContents">
<h4>Error Loading Image</h4>
<p className='description'>An error has occurred.</p>
<p className='details'></p>
</div>
</div>
</>);
}
}
LoadingIndicator.propTypes = {
percentComplete: PropTypes.number.isRequired
};
export default LoadingIndicator;

View File

@ -0,0 +1,37 @@
@value globals: "../GlobalStyles.css";
@value textActiveColor, uiGrayDark, uiGrayLight from globals;
.ToolContextMenu {
position: absolute;
background-color: white;
border: 1px solid white;
border-radius: 5px;
z-index: 1000;
display: block;
width: 160px;
}
.ToolContextMenu > ul {
list-style-type: none;
padding-left: 0;
margin: 0;
}
.ToolContextMenu > ul > li > button {
padding: 10px;
font-size: 14px;
border: none;
color: uiGrayLight;
border-radius: 3px;
outline: none;
cursor: pointer;
background: none;
}
.ToolContextMenu > ul > li > button:hover {
color: uiGrayDark;
}
.ToolContextMenu > ul > li > button:active {
color: textActiveColor;
}

View File

@ -0,0 +1,264 @@
import { Component } from 'react';
import React from 'react';
import './ViewportOverlay.css';
/*import * as cornerstone from 'cornerstone-core';
import * as cornerstoneTools from 'cornerstone-tools';*/
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
import PropTypes from 'prop-types';
import './ToolContextMenu.css';
const toolTypes = ['Bidirectional'];
let defaultDropdownItems = [
{
actionType: 'Delete',
action: ({ nearbyToolData, eventData }) => {
const element = eventData.element;
cornerstoneTools.removeToolState(
element,
nearbyToolData.toolType,
nearbyToolData.tool
);
cornerstone.updateImage(element);
}
},
{
actionType: 'bidirectionalLabel',
action: ({ nearbyToolData, eventData }) => {
const element = eventData.element;
const { tool, toolType } = nearbyToolData;
const doneCallback = () => {
cornerstone.updateImage(element);
};
const options = {
skipButton: true
};
const ToolInstance = cornerstoneTools.getToolForElement(
element,
toolType
);
ToolInstance.configuration.getMeasurementLocationCallback(
tool,
eventData,
doneCallback,
options
);
}
},
{
actionType: 'bidirectionalDescription',
action: ({ nearbyToolData, eventData }) => {
const element = eventData.element;
const { tool, toolType } = nearbyToolData;
const doneCallback = () => {
cornerstone.updateImage(element);
};
const options = {
skipButton: true,
editDescription: true
};
const ToolInstance = cornerstoneTools.getToolForElement(
element,
toolType
);
ToolInstance.configuration.getMeasurementLocationCallback(
tool,
eventData,
doneCallback,
options
);
}
}
];
function getNearbyToolData(element, coords, toolTypes) {
const nearbyTool = {};
let pointNearTool = false;
toolTypes.forEach(toolType => {
const toolData = cornerstoneTools.getToolState(element, toolType);
if (!toolData) {
return;
}
toolData.data.forEach(function(data, index) {
const toolInterface = cornerstoneTools[`${toolType}Tool`];
// TODO: vNext wants me to do this?
// Switch to cornerstoneTools.store.state.tools
const t = new toolInterface();
if (t.pointNearTool(element, data, coords)) {
pointNearTool = true;
nearbyTool.tool = data;
nearbyTool.index = index;
nearbyTool.toolType = toolType;
}
});
if (pointNearTool) {
return false;
}
});
return pointNearTool ? nearbyTool : undefined;
}
function getDropdownItems(eventData, isTouchEvent = false) {
const nearbyToolData = getNearbyToolData(
eventData.element,
eventData.currentPoints.canvas,
toolTypes
);
// Annotate tools for touch events already have a press handle to edit it, has a better UX for deleting it
if (
isTouchEvent &&
nearbyToolData &&
nearbyToolData.toolType === 'arrowAnnotate'
) {
return;
}
let dropdownItems = [];
if (nearbyToolData) {
defaultDropdownItems.forEach(function(item) {
item.params = {
eventData,
nearbyToolData
};
if (item.actionType === 'Delete') {
item.text = 'Delete measurement';
}
if (item.actionType === 'bidirectionalLabel') {
if (nearbyToolData.toolType !== 'Bidirectional') {
return;
}
item.text = `${nearbyToolData.tool.location ? 'Edit' : 'Add'} Label`;
}
if (item.actionType === 'bidirectionalDescription') {
if (
nearbyToolData.toolType !== 'Bidirectional' ||
!nearbyToolData.tool.location
) {
return;
}
item.text = `${
nearbyToolData.tool.description ? 'Edit' : 'Add'
} Description`;
}
dropdownItems.push(item);
});
}
return dropdownItems;
}
class ToolContextMenu extends Component {
constructor(props) {
super(props);
this.mainElement = React.createRef();
}
render() {
if (!this.props.toolContextMenuData) {
return '';
}
const { eventData, isTouchEvent } = this.props.toolContextMenuData;
const dropdownItems = getDropdownItems(eventData, isTouchEvent);
// Skip if there is no dropdown item
if (!dropdownItems.length) {
return '';
}
const dropdownComponents = dropdownItems.map(item => {
const itemOnClick = event => {
item.action(item.params);
this.props.onClose();
};
return (
<li key={item.actionType}>
<button className="form-action" onClick={itemOnClick}>
<span key={item.actionType}>{item.text}</span>
</button>
</li>
);
});
const position = {
top: `${eventData.currentPoints.canvas.y}px`,
left: `${eventData.currentPoints.canvas.x}px`
};
return (
<div
className="ToolContextMenu dropdown"
style={position}
ref={this.mainElement}
>
<ul className="dropdown-menu dropdown-menu-left bounded">
{dropdownComponents}
</ul>
</div>
);
}
componentDidMount = () => {
if (this.mainElement.current) {
this.updateElementPosition();
}
};
componentDidUpdate = () => {
if (this.mainElement.current) {
this.updateElementPosition();
}
};
updateElementPosition = () => {
const {
offsetParent,
offsetTop,
offsetHeight,
offsetWidth,
offsetLeft
} = this.mainElement.current;
const { eventData } = this.props.toolContextMenuData;
if (offsetTop + offsetHeight > offsetParent.offsetHeight) {
const offBoundPixels =
offsetTop + offsetHeight - offsetParent.offsetHeight;
const top = eventData.currentPoints.canvas.y - offBoundPixels;
this.mainElement.current.style.top = `${top > 0 ? top : 0}px`;
}
if (offsetLeft + offsetWidth > offsetParent.offsetWidth) {
const offBoundPixels =
offsetLeft + offsetWidth - offsetParent.offsetWidth;
const left = eventData.currentPoints.canvas.x - offBoundPixels;
this.mainElement.current.style.left = `${left > 0 ? left : 0}px`;
}
};
}
ToolContextMenu.propTypes = {
toolContextMenuData: PropTypes.object, // or null
onClose: PropTypes.func.isRequired
};
export default ToolContextMenu;

View File

@ -0,0 +1,122 @@
import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';
import { OHIF } from 'meteor/ohif:core';
// Local Modules
import { unloadHandlers } from '../../../lib/unloadHandlers';
import { ResizeViewportManager } from '../../../lib/classes/ResizeViewportManager';
import { LayoutManager } from '../../../lib/classes/LayoutManager';
import { StudyPrefetcher } from '../../../lib/classes/StudyPrefetcher';
import { StudyLoadingListener } from '../../../lib/classes/StudyLoadingListener';
import './ViewerMain.styl';
import { Component } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
Meteor.startup(() => {
window.ResizeViewportManager = window.ResizeViewportManager || new ResizeViewportManager();
// Set initial value for OHIFViewerMainRendered
// session variable. This can used in viewer main template
Session.set('OHIFViewerMainRendered', false);
});
class ViewerMain extends Component {
constructor(props) {
super(props);
this.state = {
contents: ''
};
this.setContents = this.setContents.bind(this);
}
componentDidMount() {
// Attach the Window resize listener
// Don't use jQuery here. "window.onresize" will always be null
// If its necessary, check all the code for window.onresize getter
// and change it to jQuery._data(window, 'events')['resize'].
// Otherwise this function will be probably overrided.
// See cineDialog instance.setResizeHandler function
window.addEventListener('resize', window.ResizeViewportManager.getResizeHandler());
// Add beforeUnload event handler to check for unsaved changes
window.addEventListener('beforeunload', unloadHandlers.beforeUnload);
// Set the current context
OHIF.context.set('viewer');
const { studies } = this.props;
this.studyPrefetcher = StudyPrefetcher.getInstance();
this.studyLoadingListener = StudyLoadingListener.getInstance();
this.studyLoadingListener.clear();
this.studyLoadingListener.addStudies(studies);
OHIF.viewerbase.layoutManager = new LayoutManager(this.setContents, studies);
this.studyPrefetcher.setStudies(studies);
Session.set('OHIFViewerMainRendered', Math.random());
}
setContents(component, data) {
const Comp = this.props.component;
const contents = (<Comp data={data}/>);
this.setState({
contents
});
}
render() {
return (
<div className="viewerMain">
{this.state.contents}
</div>
);
}
componentWillUnmount() {
OHIF.log.info('viewerMain onDestroyed');
// Remove the Window resize listener
window.removeEventListener('resize', window.ResizeViewportManager.getResizeHandler());
// Remove beforeUnload event handler...
window.removeEventListener('beforeunload', unloadHandlers.beforeUnload);
// Destroy the synchronizer used to update reference lines
OHIF.viewer.updateImageSynchronizer.destroy();
delete OHIF.viewerbase.layoutManager;
ProtocolEngine = null;
Session.set('OHIFViewerMainRendered', false);
// Stop prefetching when we close the viewer
this.studyPrefetcher.destroy();
// Destroy stack loading listeners when we close the viewer
this.studyLoadingListener.clear();
// Clear references to all stacks in the StackManager
OHIF.viewerbase.stackManager.clearStacks();
// @TypeSafeStudies
// Clears OHIF.viewer.Studies collection
OHIF.viewer.Studies.removeAll();
// @TypeSafeStudies
// Clears OHIF.viewer.StudyMetadataList collection
OHIF.viewer.StudyMetadataList.removeAll();
// Reset the current context
OHIF.context.set(null);
}
}
ViewerMain.propTypes = {
studies: PropTypes.array.isRequired
}
export default ViewerMain;

View File

@ -0,0 +1,38 @@
.ViewportOverlay {
width: 100%;
height: 100%;
position: absolute;
top: 0px;
left: 0px;
z-index: 2;
}
.ViewportOverlay .overlay-element {
position: absolute;
color: lightblue;
text-shadow: 1px 1px black;
}
.ViewportOverlay .overlay-element > span {
display: block;
}
.ViewportOverlay .top-right {
top: 10px;
right: 25px; /* room for scrollbar */
}
.ViewportOverlay .top-left {
top: 10px;
left: 10px;
}
.ViewportOverlay .bottom-right {
bottom: 10px;
right: 30px;
}
.ViewportOverlay .bottom-left {
bottom: 10px;
left: 10px;
}

View File

@ -0,0 +1,52 @@
import { Component } from 'react';
import React from 'react';
import './ViewportOverlay.css';
import PropTypes from 'prop-types';
class ViewportOverlay extends Component {
render() {
const scale = Math.round(this.props.viewport.scale * 100) / 100;
const imageId = this.props.imageId;
/*const patientId = cornerstone.metaData.get('00100020', imageId);
const studyDate = cornerstone.metaData.get('00080020', imageId);
const collection = cornerstone.metaData.get('00131010', imageId);
const studyDescription = cornerstone.metaData.get('00081030', imageId);*/
const windowWidth = Math.round(this.props.viewport.voi.windowWidth);
const windowCenter = Math.round(this.props.viewport.voi.windowCenter);
const imagesLeft =
this.props.stack.imageIds.length - this.props.numImagesLoaded;
return (
<div className="ViewportOverlay">
<div className="top-left overlay-element">
<span>{this.props.patientId}</span>
<span>{this.props.studyDate}</span>
<span>{this.props.studyDescription}</span>
<span>
{imagesLeft > 0 ? `${imagesLeft} images remaining...` : ''}
</span>
</div>
<div className="bottom-left overlay-element">Zoom: {scale}</div>
<div className="bottom-right overlay-element">
<span>
WW/WC: {windowWidth} / {windowCenter}
</span>
<span>
Image: {this.props.stack.currentImageIdIndex + 1} /{' '}
{this.props.stack.imageIds.length}
</span>
</div>
</div>
);
}
}
ViewportOverlay.propTypes = {
viewport: PropTypes.object.isRequired,
imageId: PropTypes.string.isRequired,
stack: PropTypes.object.isRequired
};
export default ViewportOverlay;

View File

@ -1,18 +0,0 @@
<template name="gridLayout">
<div id='imageViewerViewports'>
{{#each viewport in viewports}}
<div class="viewportContainer {{getClass @index}} {{ activeClass @index }}" style="height:{{height}}%;width:{{width}}%;">
{{ #if eq viewport.plugin 'cornerstone'}}
<div class="removable">
{{>imageViewerViewport (clone viewport)}}
</div>
{{ else }}
<div class="viewport-plugin-{{viewport.plugin}}"
style="height:100%; width: 100%">
</div>
{{ /if }}
{{>seriesQuickSwitch (clone this viewport=viewport viewportIndex=@index)}}
</div>
{{/each}}
</div>
</template>

View File

@ -1,78 +0,0 @@
import { OHIF } from 'meteor/ohif:core';
import { Template } from 'meteor/templating';
const TOP_CLASS = 'top';
const BOTTOM_CLASS = 'bottom';
const MIDDLE_CLASS = 'middle';
Template.gridLayout.helpers({
// Get the height percentage for each viewport
height() {
const instance = Template.instance();
const rows = instance.data.rows || 1;
return 100 / rows;
},
// Get the width percentage for each viewport
width() {
const instance = Template.instance();
const columns = instance.data.columns || 1;
return 100 / columns;
},
// Get class for each viewport, so each app
// using ohif-viewerbase can style on their own
getClass(index) {
const { rows, columns } = this;
if (rows === 1) {
return `${TOP_CLASS} ${BOTTOM_CLASS}`;
}
const actualRow = Math.floor(index / columns);
if ( actualRow === 0 ) {
return TOP_CLASS;
}
if ( actualRow + 1 === rows ) {
return BOTTOM_CLASS;
}
return MIDDLE_CLASS;
},
activeClass(index) {
if (Session.get('activeViewport') === index) {
return 'active';
};
},
// Return the viewports list
viewports() {
const instance = Template.instance();
const rows = instance.data.rows;
const columns = instance.data.columns;
const numViewports = rows * columns;
const viewportData = instance.data.viewportData;
const numViewportsWithData = viewportData.length;
// Check if the viewportData length is different from the given
if (numViewportsWithData < numViewports) {
// Add the missing viewports
var difference = numViewports - numViewportsWithData;
for (var i = 0; i < difference; i++) {
viewportData.push({
viewportIndex: numViewportsWithData + i + 1,
rows,
columns
});
}
} else if (numViewportsWithData > numViewports) {
// Remove the additional viewports
return viewportData.slice(0, numViewports);
}
// Return the viewports
return viewportData;
}
});

View File

@ -5,6 +5,10 @@ import { OHIF } from 'meteor/ohif:core';
import { setActiveViewport } from '../../../lib/setActiveViewport';
import { switchToImageByIndex } from '../../../lib/switchToImageByIndex';
import { Component } from 'react';
import React from 'react';
const slideTimeoutTime = 40;
let slideTimeout;

View File

@ -0,0 +1,17 @@
import ViewerMain from './ViewerMain/ViewerMain.js';
import GridLayout from './GridLayout/GridLayout.js';
import LoadingIndicator from './LoadingIndicator/LoadingIndicator.js';
import CornerstoneViewport from './CornerstoneViewport.js';
import ImageScrollbar from './ImageScrollbar.js';
import ToolContextMenu from './ToolContextMenu.js';
import ViewportOverlay from './ViewportOverlay.js';
export {
ViewerMain,
GridLayout,
LoadingIndicator,
CornerstoneViewport,
ImageScrollbar,
ToolContextMenu,
ViewportOverlay,
};

View File

@ -1,14 +0,0 @@
<template name="loadingIndicator">
<div class="imageViewerLoadingIndicator loadingIndicator">
<div class="indicatorContents">
<p>Loading... <i class="fa fa-spin fa-circle-o-notch fa-fw"></i> {{percentComplete}}</p>
</div>
</div>
<div class="imageViewerErrorLoadingIndicator loadingIndicator">
<div class="indicatorContents">
<h4>Error Loading Image</h4>
<p class='description'>An error has occurred.</p>
<p class='details'></p>
</div>
</div>
</template>

View File

@ -55,6 +55,9 @@ const createDropdown = function(eventData, isTouchEvent = false) {
}
};
/*
TODO[react]: Migrate these to ViewerMain React Component
Template.viewerMain.events({
'cornerstonetoolsmouseclick .imageViewerViewport'(event) {
const { originalEvent } = event;
@ -69,4 +72,4 @@ Template.viewerMain.events({
const eventData = originalEvent.detail;
createDropdown(eventData, true);
}
});
});*/

View File

@ -1,6 +0,0 @@
<template name="viewerMain">
<div class="viewerMain">
<div id='layoutManagerTarget'>
</div>
</div>
</template>

View File

@ -1,91 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Session } from 'meteor/session';
import { OHIF } from 'meteor/ohif:core';
// Local Modules
import { unloadHandlers } from '../../../lib/unloadHandlers';
import { ResizeViewportManager } from '../../../lib/classes/ResizeViewportManager';
import { LayoutManager } from '../../../lib/classes/LayoutManager';
import { StudyPrefetcher } from '../../../lib/classes/StudyPrefetcher';
import { StudyLoadingListener } from '../../../lib/classes/StudyLoadingListener';
Meteor.startup(() => {
window.ResizeViewportManager = window.ResizeViewportManager || new ResizeViewportManager();
// Set initial value for OHIFViewerMainRendered
// session variable. This can used in viewer main template
Session.set('OHIFViewerMainRendered', false);
});
Template.viewerMain.onCreated(() => {
// Attach the Window resize listener
// Don't use jQuery here. "window.onresize" will always be null
// If its necessary, check all the code for window.onresize getter
// and change it to jQuery._data(window, 'events')['resize'].
// Otherwise this function will be probably overrided.
// See cineDialog instance.setResizeHandler function
window.addEventListener('resize', window.ResizeViewportManager.getResizeHandler());
// Add beforeUnload event handler to check for unsaved changes
window.addEventListener('beforeunload', unloadHandlers.beforeUnload);
// Set the current context
OHIF.context.set('viewer');
});
Template.viewerMain.onRendered(() => {
const instance = Template.instance();
const { studies } = instance.data;
const parentElement = instance.$('#layoutManagerTarget').get(0);
const studyPrefetcher = StudyPrefetcher.getInstance();
instance.studyPrefetcher = studyPrefetcher;
instance.studyLoadingListener = StudyLoadingListener.getInstance();
instance.studyLoadingListener.clear();
instance.studyLoadingListener.addStudies(studies);
OHIF.viewerbase.layoutManager = new LayoutManager(parentElement, studies);
studyPrefetcher.setStudies(studies);
Session.set('OHIFViewerMainRendered', Math.random());
});
Template.viewerMain.onDestroyed(() => {
const instance = Template.instance();
OHIF.log.info('viewerMain onDestroyed');
// Remove the Window resize listener
window.removeEventListener('resize', window.ResizeViewportManager.getResizeHandler());
// Remove beforeUnload event handler...
window.removeEventListener('beforeunload', unloadHandlers.beforeUnload);
// Destroy the synchronizer used to update reference lines
OHIF.viewer.updateImageSynchronizer.destroy();
delete OHIF.viewerbase.layoutManager;
ProtocolEngine = null;
Session.set('OHIFViewerMainRendered', false);
// Stop prefetching when we close the viewer
instance.studyPrefetcher.destroy();
// Destroy stack loading listeners when we close the viewer
instance.studyLoadingListener.clear();
// Clear references to all stacks in the StackManager
OHIF.viewerbase.stackManager.clearStacks();
// @TypeSafeStudies
// Clears OHIF.viewer.Studies collection
OHIF.viewer.Studies.removeAll();
// @TypeSafeStudies
// Clears OHIF.viewer.StudyMetadataList collection
OHIF.viewer.StudyMetadataList.removeAll();
// Reset the current context
OHIF.context.set(null);
});

View File

@ -0,0 +1,19 @@
import { Component } from 'react';
import React from 'react';
//import PropTypes from 'prop-types';
import './ViewportOrientationMarkers.styl';
class ViewportOrientationMarkers extends Component {
render() {
return (
<div class="viewportOrientationMarkers noselect">
<div class="topMid orientationMarker">
</div>
<div class="leftMid orientationMarker">
</div>
</div>
);
}
};
export default ViewportOrientationMarkers;

View File

@ -1,8 +0,0 @@
<template name="viewportOrientationMarkers">
<div class="viewportOrientationMarkers noselect">
<div class="topMid orientationMarker">
</div>
<div class="leftMid orientationMarker">
</div>
</div>
</template>

View File

@ -6,6 +6,9 @@ import { Viewerbase } from '../namespace';
import './collections';
import './lib/debugReactivity';
import components from './components/index.js';
Viewerbase.components = components;
/**
* Exported Functions

View File

@ -8,6 +8,8 @@ import { $ } from 'meteor/jquery';
import { OHIF } from 'meteor/ohif:core';
import viewer from '../../components/viewer';
const PLUGIN_CORNERSTONE = 'cornerstone';
let isInteractingWithViewport = false;
@ -28,14 +30,15 @@ export class LayoutManager {
* @param {DOM element} parentNode DOM element representing the parent node, which wraps the Layout Manager content
* @param {Array} studies Array of studies objects that will be rendered in the Viewer. Each object will be rendered in a div.imageViewerViewport
*/
constructor(parentNode, studies) {
constructor(setContents, studies) {
OHIF.log.info('LayoutManager constructor');
this.setContents = setContents;
this.observer = new Tracker.Dependency();
this.parentNode = parentNode;
this.studies = studies;
this.viewportData = [];
this.layoutTemplateName = 'gridLayout';
this.layoutTemplate = viewer.GridLayout;
this.layoutProps = {
rows: 1,
columns: 1
@ -209,11 +212,10 @@ export class LayoutManager {
data.viewportData.push(viewportDataAndLayoutProps);
});
const layoutTemplate = Template[this.layoutTemplateName];
this.parentNode.innerHTML = '';
this.updateLayoutClass();
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
const component = GridLayout;
this.setContents(component, data)
this.updateSession();
@ -276,6 +278,8 @@ export class LayoutManager {
container.appendChild(newViewportContainer);
Blaze.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
} else {
newViewportContainer.className = `viewport-plugin-${plugin}`;
newViewportContainer.style.width = '100%';
@ -314,10 +318,8 @@ export class LayoutManager {
columns: 1
};
const layoutTemplate = Template.gridLayout;
this.parentNode.innerHTML = '';
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
const component = GridLayout;
this.setContents(component, data)
this.isZoomed = true;
this.zoomedViewportIndex = viewportIndex;

View File

@ -17,7 +17,7 @@ export function updateOrientationMarkers(element, viewport) {
// Updates the orientation labels on the viewport
const enabledElement = cornerstone.getEnabledElement(element);
const imagePlane = cornerstone.metaData.get('imagePlane', enabledElement.image.imageId);
if (!imagePlane || !imagePlane.rowCosines || !imagePlane.columnCosines) {
return;
}
@ -42,6 +42,8 @@ export function updateOrientationMarkers(element, viewport) {
markers.left = cornerstoneTools.orientation.invertOrientationString(markers.left);
}
// TODO[react]: This should set values in the redux store
// Get the viewport orientation marker DOM elements
const viewportOrientationMarkers = $(element).siblings('.viewportOrientationMarkers');
const topMarker = viewportOrientationMarkers.find('.topMid');

View File

@ -125,29 +125,16 @@ Package.onUse(function(api) {
api.addFiles('client/components/viewer/imageViewerViewport/imageViewerViewport.js', 'client');
api.addFiles('client/components/viewer/imageViewerViewport/imageViewerViewport.styl', 'client');
api.addFiles('client/components/viewer/gridLayout/gridLayout.html', 'client');
api.addFiles('client/components/viewer/gridLayout/gridLayout.js', 'client');
api.addFiles('client/components/viewer/gridLayout/gridLayout.styl', 'client');
api.addFiles('client/components/viewer/loadingIndicator/loadingIndicator.html', 'client');
api.addFiles('client/components/viewer/loadingIndicator/loadingIndicator.js', 'client');
api.addFiles('client/components/viewer/loadingIndicator/loadingIndicator.styl', 'client');
api.addFiles('client/components/viewer/annotationDialogs/annotationDialogs.html', 'client');
api.addFiles('client/components/viewer/annotationDialogs/annotationDialogs.js', 'client');
api.addFiles('client/components/viewer/annotationDialogs/annotationDialogs.styl', 'client');
api.addFiles('client/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.html', 'client');
api.addFiles('client/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.styl', 'client');
api.addFiles('client/components/viewer/viewportOverlay/viewportOverlay.html', 'client');
api.addFiles('client/components/viewer/viewportOverlay/viewportOverlay.js', 'client');
api.addFiles('client/components/viewer/viewportOverlay/viewportOverlay.styl', 'client');
api.addFiles('client/components/viewer/viewerMain/viewerMain.html', 'client');
api.addFiles('client/components/viewer/viewerMain/viewerMain.js', 'client');
api.addFiles('client/components/viewer/viewerMain/viewerMain.styl', 'client');
api.addFiles('client/components/viewer/toolContextMenu/toolContextMenu.js', 'client');
api.addFiles('client/components/viewer/imageControls/imageControls.html', 'client');