feat(plugins): Add initial version of ViewportPlugin class (#242)

* Add initial version of ViewportPlugin class
* Move entrypoints from OHIFPlugin to OHIF.plugins
* Add URL route to DICOMWeb server for Standalone Viewer
This commit is contained in:
Erik Ziegler authored and GitHub committed 2018-08-08 18:00:53 +02:00
1 parent ced47c765f
commit 53cd12a16a
18 files changed
+708 -562

No files matched your search

@@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';import { Blaze } from 'meteor/blaze';
import { Meteor } from 'meteor/meteor';
import { Blaze } from 'meteor/blaze';
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { Template } from 'meteor/templating';
@@ -7,6 +8,8 @@ import { $ } from 'meteor/jquery';
import { OHIF } from 'meteor/ohif:core';
const PLUGIN_CORNERSTONE = 'cornerstone';
let isInteractingWithViewport = false;
Meteor.startup(() => {
const setInteracting = flag => {
@@ -41,11 +44,16 @@ export class LayoutManager {
this.isZoomed = false;
const updateSessionFn = () => Tracker.afterFlush(() => {
Session.set('LayoutManagerUpdated', Math.random());
const updateSessionFn = () => {
const random = Math.random();
Session.set('LayoutManagerUpdated', random);
this.observer.changed();
};
this.updateSession = _.throttle(updateSessionFn, 300, {
leading: true,
trailing: false
});
this.updateSession = _.throttle(updateSessionFn, 300);
}
/**
@@ -121,7 +129,8 @@ export class LayoutManager {
studyInstanceUid,
seriesInstanceUid,
displaySetInstanceUid,
sopInstanceUid
sopInstanceUid,
plugin: PLUGIN_CORNERSTONE
};
additionalData.push(data);
@@ -181,6 +190,12 @@ export class LayoutManager {
this.setDefaultViewportData();
}
this.viewportData.forEach(data => {
if (!data.plugin) {
data.plugin = PLUGIN_CORNERSTONE;
}
})
// imageViewerViewports occasionally needs relevant layout data in order to set
// the element style of the viewport in question
const layoutProps = this.layoutProps;
@@ -190,13 +205,13 @@ export class LayoutManager {
this.viewportData.forEach(viewportData => {
const viewportDataAndLayoutProps = $.extend(viewportData, layoutProps);
data.viewportData.push(viewportDataAndLayoutProps);
});
const layoutTemplate = Template[this.layoutTemplateName];
const $parentNode = $(this.parentNode);
$parentNode.html('');
this.parentNode.innerHTML = '';
this.updateLayoutClass();
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
@@ -218,12 +233,15 @@ export class LayoutManager {
OHIF.log.info(`LayoutManager rerenderViewportWithNewDisplaySet: ${viewportIndex}`);
// The parent container is identified because it is later removed from the DOM
const element = $('.imageViewerViewport').eq(viewportIndex);
const container = element.parents('.viewportContainer').get(0);
const container = $('.viewportContainer').get(viewportIndex);
// Record the current viewportIndex so this can be passed into the re-rendering call
data.viewportIndex = viewportIndex;
// If we have been provided with a plugin to use, use it.
// Otherwise, use whichever plugin is currently in use in this viewport.
const plugin = data.plugin || this.viewportData[viewportIndex].plugin;
// Update the dictionary of loaded displaySet for the specified viewport
this.viewportData[viewportIndex] = {
viewportIndex: viewportIndex,
@@ -231,27 +249,40 @@ export class LayoutManager {
seriesInstanceUid: data.seriesInstanceUid,
studyInstanceUid: data.studyInstanceUid,
renderedCallback: data.renderedCallback,
currentImageIdIndex: data.currentImageIdIndex || 0
currentImageIdIndex: data.currentImageIdIndex || 0,
plugin,
};
// Remove the hover styling
element.find('canvas').not('.magnifyTool').removeClass('faded');
// Remove the whole template, add in the new one
const viewportContainer = element.parents('.removable');
const newViewportContainer = document.createElement('div');
newViewportContainer.className = 'removable';
// Remove the parent element of the template
// This is a workaround since otherwise Blaze UI onDestroyed doesn't fire
viewportContainer.remove();
container.appendChild(newViewportContainer);
// Render and insert the template
Blaze.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
if (plugin === PLUGIN_CORNERSTONE) {
// Remove the hover styling
const element = $(container).find('.imageViewerViewport');
element.find('canvas').not('.magnifyTool').removeClass('faded');
// Remove the whole template, add in the new one
const viewportContainer = element.parents('.removable');
newViewportContainer.className = 'removable';
// Remove the parent element of the template
// This is a workaround since otherwise Blaze UI onDestroyed doesn't fire
viewportContainer.remove();
container.appendChild(newViewportContainer);
Blaze.renderWithData(Template.imageViewerViewport, data, newViewportContainer);
} else {
newViewportContainer.className = `viewport-plugin-${plugin}`;
newViewportContainer.style.width = '100%';
newViewportContainer.style.height = '100%';
container.innerHTML = '';
container.appendChild(newViewportContainer);
}
this.updateSession();
}
@@ -283,7 +314,7 @@ export class LayoutManager {
const layoutTemplate = Template.gridLayout;
$(this.parentNode).html('');
this.parentNode.innerHTML = '';
Blaze.renderWithData(layoutTemplate, data, this.parentNode);
this.isZoomed = true;
@@ -1,5 +1,4 @@
// TODO: Add plugin reloader
class OHIFPlugin {
export class OHIFPlugin {
// TODO: this class is still under development and will
// likely change in the near future
constructor () {
@@ -25,10 +24,12 @@ class OHIFPlugin {
// reload all the dependency scripts and also
// the main plugin script url.
static reloadPlugin(plugin) {
plugin.scriptURLs = plugin.scriptURLs || {};
plugin.scriptURLs.forEach(scriptURL => {
this.loadScript(scriptURL).onload = function() {}
});
console.warn(`reloadPlugin: ${plugin.name}`);
if (plugin.scriptURLs && plugin.scriptURLs.length) {
plugin.scriptURLs.forEach(scriptURL => {
this.loadScript(scriptURL).onload = function() {}
});
}
let scriptURL = plugin.url;
@@ -37,20 +38,11 @@ class OHIFPlugin {
}
this.loadScript(scriptURL).onload = function() {
if (OHIFPlugin.entryPoints[plugin.name]) {
OHIFPlugin.entryPoints[plugin.name]();
const entryPointFunction = OHIF.plugins.entryPoints[plugin.name];
if (entryPointFunction) {
entryPointFunction();
}
}
}
}
// each plugin registers an entry point function to be called
// when the loading is complete (called above in reloadPlugin).
// TODO: Move to OHIF.plugins.entryPoints?
OHIFPlugin.entryPoints = {};
export { OHIFPlugin };
// TODO: Should we remove this? Authors should be able to use new 'OHIF.viewerbase.OHIFPlugin()'
window.OHIFPlugin = OHIFPlugin;
@@ -0,0 +1,169 @@
import { Session } from 'meteor/session';
import { Tracker } from 'meteor/tracker';
import { OHIF } from 'meteor/ohif:core';
import { OHIFPlugin } from "./OHIFPlugin";
export class ViewportPlugin extends OHIFPlugin {
constructor(name) {
super();
this.name = name;
this._destroyed = false;
this._setupListeners();
}
/**
* Retrieve a Display Set for a specific viewport by viewport index,
* if one is already displayed in the viewport.
*
* @static
* @param {Number} viewportIndex
* @return {undefined|ImageSet}
*/
static getDisplaySet(viewportIndex) {
// TODO: Move layoutManager from viewerbase to viewer
const { layoutManager } = OHIF.viewerbase;
const viewportData = layoutManager.viewportData[viewportIndex];
const { studyInstanceUid, displaySetInstanceUid } = viewportData;
const studyMetadata = OHIF.viewer.StudyMetadataList.findBy({ studyInstanceUID: studyInstanceUid });
return studyMetadata.findDisplaySet(displaySet => {
return displaySet.displaySetInstanceUid === displaySetInstanceUid;
});
}
/**
* Set up the viewport using the plugin.
*
* This should be implemented by the child class.
*
* @abstract
*
* @param {HTMLElement} div
* @param {ImageSet} displaySet
* @param {Object} viewportDetails
*/
setupViewport(div, displaySet, viewportDetails) {
throw new Error('You must override this method!');
}
/**
* Switch a single viewport to use the current ViewportPlugin
*
* @param {Number} viewportIndex The viewport to switch to the current plugin type
*/
setViewportToPlugin(viewportIndex) {
if (!this.name) {
throw new Error('ViewportPlugin subclasses must have a name');
}
const { layoutManager } = OHIF.viewerbase;
const viewportData = layoutManager.viewportData[viewportIndex];
if (viewportData.plugin === this.name) {
OHIF.log.info(`setViewportToPlugin: Viewport ${viewportIndex} already set to plugin ${this.name}`);
return;
}
viewportData.plugin = this.name;
layoutManager.rerenderViewportWithNewDisplaySet(viewportIndex, viewportData);
}
/**
* Runs 'setupViewport' for the ViewportPlugin on all viewports which should
* be rendered by this plugin, but have not yet been initialized. Viewports
* which already contain contents are skipped.
*
* @private
*/
_initEmptyPluginViewports() {
if (!this.name) {
throw new Error('ViewportPlugin subclasses must have a name');
}
// Find all Viewport HTMLElements currently using this plugin
const pluginDivs = Array.from(document.querySelectorAll(`.viewport-plugin-${this.name}`));
// If there are no Viewports using this plugin, stop here
if (!pluginDivs.length) {
return;
}
const emptyPluginDivs = pluginDivs.filter(div => {
// Keep only divs owned by the plugin which have no contents
return div.innerHTML.trim() === '';
});
OHIF.log.info(`${this.name}: Initializing ${emptyPluginDivs.length} viewports`);
// Retrieve the list of all viewports, so we can figure out the viewport details
const allViewports = Array.from(document.querySelectorAll('.viewportContainer'));
emptyPluginDivs.forEach(div => {
// Identify the Viewport index, and any display set that is currently
// hung in the viewport
const viewportIndex = allViewports.indexOf(div.parentNode);
const viewportDetails = { viewportIndex };
const displaySet = ViewportPlugin.getDisplaySet(viewportIndex);
// Use the plugin's setupViewport function to render the contents
// of this viewport.
this.setupViewport(div, viewportDetails, displaySet);
});
}
/**
* Listen for changes to the viewport layout which would necessitate a
* rerendering of the viewports. When this happens, re-render all viewports
* which are using this plugin.
*
* @private
*/
_setupListeners() {
if (!this.name) {
throw new Error('ViewportPlugin subclasses must have a name');
}
console.warn(`_setupListeners: ${this.name}`);
// TODO: Stop using Meteor's reactivity here
Tracker.autorun((computation) => {
const random = Session.get('LayoutManagerUpdated');
console.warn(`LayoutManagerUpdated: ${this.name}: ${random}`);
// Bail out if this is the first time the autorun
// executes (i.e. when it is being defined).
//
// Note: This has to be checked after the dependency on the
// Session variable above, or the reactive dependency will not
// be established.
if (computation.firstRun === true) {
return;
}
// In case we need to disable the use
// of this plugin, we can also stop the
// reactive computation by setting
// this.destroyed to true.
if (this._destroyed === true) {
computation.stop();
}
// Identify all viewports which should be
// rendered by the ViewportPlugin, and render
// them.
this._initEmptyPluginViewports();
});
}
/**
* Stop listening for changes to the viewport layout in order to
* automatically rerender viewports setup for use by this plugin.
*/
stopListeners() {
this._destroyed = true;
}
}
@@ -0,0 +1,18 @@
import { OHIF } from 'meteor/ohif:core';
import { OHIFPlugin } from './OHIFPlugin';
import { ViewportPlugin } from './ViewportPlugin';
// Each plugin registers an entry point function to be called
// when the loading is complete.
const plugins = {
OHIFPlugin,
ViewportPlugin,
entryPoints: {}
};
// TODO: When we reorganize the packages, we should figure out where to put this.
OHIF.plugins = plugins;
export default plugins;