wip on react transition
This commit is contained in:
parent
5a4deedda8
commit
7425c2beb8
@ -51,3 +51,4 @@ cultofcoders:persistent-session
|
||||
shell-server@0.3.1
|
||||
underscore
|
||||
meteortesting:mocha
|
||||
react-template-helper
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)}}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
177
OHIFViewer/package-lock.json
generated
177
OHIFViewer/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
6
Packages/ohif-viewerbase/client/components/index.js
Normal file
6
Packages/ohif-viewerbase/client/components/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
import viewer from './viewer/index.js';
|
||||
|
||||
export {
|
||||
viewer
|
||||
};
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
.CornerstoneViewport {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
|
||||
|
||||
17
Packages/ohif-viewerbase/client/components/viewer/index.js
Normal file
17
Packages/ohif-viewerbase/client/components/viewer/index.js
Normal 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,
|
||||
};
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
});*/
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
<template name="viewerMain">
|
||||
<div class="viewerMain">
|
||||
<div id='layoutManagerTarget'>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -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);
|
||||
});
|
||||
@ -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;
|
||||
@ -1,8 +0,0 @@
|
||||
<template name="viewportOrientationMarkers">
|
||||
<div class="viewportOrientationMarkers noselect">
|
||||
<div class="topMid orientationMarker">
|
||||
</div>
|
||||
<div class="leftMid orientationMarker">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -6,6 +6,9 @@ import { Viewerbase } from '../namespace';
|
||||
|
||||
import './collections';
|
||||
import './lib/debugReactivity';
|
||||
import components from './components/index.js';
|
||||
|
||||
Viewerbase.components = components;
|
||||
|
||||
/**
|
||||
* Exported Functions
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user