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,10 +1,16 @@
<template name="gridLayout">
<div id='imageViewerViewports'>
{{#each viewport in viewports}}
<div class="viewportContainer {{getClass @index}}" style="height:{{height}}%;width:{{width}}%;">
<div class="removable">
{{>imageViewerViewport (clone viewport)}}
</div>
<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}}
@@ -30,7 +30,7 @@ Template.gridLayout.helpers({
}
const actualRow = Math.floor(index / columns);
if ( actualRow === 0 ) {
return TOP_CLASS;
}
@@ -40,7 +40,13 @@ Template.gridLayout.helpers({
return MIDDLE_CLASS;
},
activeClass(index) {
if (Session.get('activeViewport') === index) {
return 'active';
};
},
// Return the viewports list
viewports() {
const instance = Template.instance();
@@ -65,8 +65,8 @@ Template.imageControls.events({
// Using the slider in an inactive viewport
// should cause that viewport to become active
const $slider = $(event.currentTarget);
const newActiveElement = $slider.parents('.viewportContainer').find('.imageViewerViewport');
setActiveViewport(newActiveElement);
const viewportContainer = $slider.parents('.viewportContainer');
setActiveViewport(viewportContainer);
// Subtract 1 here since the slider goes from 1 to N images
// But the stack indexing starts at 0
@@ -21,6 +21,8 @@ const allCornerstoneEvents = ['click', 'cornerstonetoolsmousedown', 'cornerstone
'cornerstonetoolsmousewheel', 'cornerstonetoolsdoubletap', 'cornerstonetoolstouchpress',
'cornerstonetoolsmultitouchstart', 'cornerstonetoolsmultitouchstartactive', 'cornerstonetoolsmultitouchdrag'];
const PLUGIN_CORNERSTONE = 'cornerstone';
/**
* This function loads a study series into a viewport element.
*
@@ -40,7 +42,7 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
// Get the current element and it's index in the list of all viewports
// The viewport index is often used to store information about a viewport element
const element = data.element;
const viewportIndex = $('.imageViewerViewport').index(element);
const viewportIndex = templateData.viewportIndex;
const layoutManager = OHIF.viewerbase.layoutManager;
layoutManager.viewportData = layoutManager.viewportData || {};
@@ -115,7 +117,8 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
displaySetInstanceUid,
currentImageIdIndex,
viewport: viewport || data.viewport,
viewportIndex
viewportIndex,
plugin: PLUGIN_CORNERSTONE
};
// Handle the case where the imageId isn't loaded correctly and the
@@ -461,7 +464,9 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
// This is done to ensure that the active element has the current
// focus, so that keyboard events are triggered.
if (viewportIndex === Session.get('activeViewport')) {
setActiveViewport(element);
const viewportContainer = $element.parents('.viewportContainer');
setActiveViewport(viewportContainer);
}
// Run any renderedCallback that exists in the data context
@@ -469,9 +474,6 @@ const loadDisplaySetIntoViewport = (data, templateData) => {
data.renderedCallback(element);
}
// Update the LayoutManagerUpdated session key
layoutManager.updateSession();
// Check if it has after loadAndCacheImage callback
if (typeof callbacks.after === 'function') {
OHIF.log.info('imageViewerViewport after callback');
@@ -624,8 +626,8 @@ Template.imageViewerViewport.onRendered(function() {
viewportIndexToZoom = layoutManager.zoomedViewportIndex || 0;
}
// Set zoomed viewport as active...
const element = $('.imageViewerViewport').get(viewportIndexToZoom);
setActiveViewport(element);
const viewportContainer = $('.viewportContainer').get(viewportIndexToZoom);
setActiveViewport(viewportContainer);
});
}
@@ -682,6 +684,8 @@ Template.imageViewerViewport.onDestroyed(function() {
Template.imageViewerViewport.events({
'OHIFActivateViewport .imageViewerViewport'(event) {
OHIF.log.info('imageViewerViewport OHIFActivateViewport');
setActiveViewport(event.currentTarget);
const viewportContainer = $(event.currentTarget).parents('.viewportContainer').get(0);
setActiveViewport(viewportContainer);
}
});
+2 -2
View File
@@ -227,8 +227,8 @@ import { InstanceMetadata } from './lib/classes/metadata/InstanceMetadata';
import { StudySummary } from './lib/classes/metadata/StudySummary';
Viewerbase.metadata = { StudyMetadata, SeriesMetadata, InstanceMetadata, StudySummary };
import { OHIFPlugin } from './lib/classes/OHIFPlugin';
Viewerbase.OHIFPlugin = OHIFPlugin;
import { plugins } from './lib/classes/plugins/';
Viewerbase.plugins = plugins;
// TypeSafeCollection
import { TypeSafeCollection } from './lib/classes/TypeSafeCollection';
@@ -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;
@@ -8,18 +8,18 @@ const loadPreviousActivePanel = () => {
let currentIndex = Session.get('activeViewport');
currentIndex--;
const $viewports = $('.imageViewerViewport');
const $viewports = $('.viewportContainer');
const numViewports = $viewports.length;
if (currentIndex < 0) {
currentIndex = numViewports - 1;
}
const element = $viewports.get(currentIndex);
if (!element) {
const viewportContainer = $viewports.get(currentIndex);
if (!viewportContainer) {
return;
}
setActiveViewport(element);
setActiveViewport(viewportContainer);
};
const loadNextActivePanel = () => {
@@ -27,18 +27,18 @@ const loadNextActivePanel = () => {
let currentIndex = Session.get('activeViewport');
currentIndex++;
const $viewports = $('.imageViewerViewport');
const $viewports = $('.viewportContainer');
const numViewports = $viewports.length;
if (currentIndex >= numViewports) {
currentIndex = 0;
}
const element = $viewports.get(currentIndex);
if (!element) {
const viewportContainer = $viewports.get(currentIndex);
if (!viewportContainer) {
return;
}
setActiveViewport(element);
setActiveViewport(viewportContainer);
};
/**
@@ -6,21 +6,17 @@ import { OHIF } from 'meteor/ohif:core';
import { StudyPrefetcher } from './classes/StudyPrefetcher';
import { displayReferenceLines } from './displayReferenceLines';
const PLUGIN_CORNERSTONE = 'cornerstone';
/**
* Sets a viewport element active
* @param {node} element DOM element to be activated or viewportIndex
*/
export function setActiveViewport(element) {
const $viewerports = $('.imageViewerViewport');
const $viewports = $('.viewportContainer');
const viewportIndex = $viewports.index(element);
let viewportIndex;
if (typeof element === 'number') {
viewportIndex = element;
} else {
viewportIndex = $viewerports.index(element);
}
const $element = $viewerports.eq(viewportIndex);
const $element = $viewports.eq(viewportIndex);
if (!$element.length) {
OHIF.log.info('setActiveViewport element does not exist');
return;
@@ -38,25 +34,16 @@ export function setActiveViewport(element) {
// with the viewport index that it was fired from.
Session.set('activeViewport', viewportIndex);
const randomId = Random.id();
// Update the Session variable to inform that a viewport is active
Session.set('viewportActivated', randomId);
// Update the Session variable to the UI re-renders
Session.set('LayoutManagerUpdated', randomId);
// Add the 'active' class to the parent container to highlight the active viewport
$('#imageViewerViewports .viewportContainer').removeClass('active');
$element.parents('.viewportContainer').addClass('active');
// Finally, enable stack prefetching and hide the reference lines from
// the newly activated viewport that has a canvas
const { layoutManager } = OHIF.viewerbase;
const viewportData = layoutManager.viewportData[viewportIndex];
if ($element.find('canvas').length) {
if (viewportData.plugin === PLUGIN_CORNERSTONE &&
$element.find('canvas').length) {
// Cornerstone Tools compare DOM elements (check getEnabledElement cornerstone function)
// so we can't pass a jQuery object as an argument, otherwise it throws an excepetion
const domElement = $element.get(0);
const domElement = $element.find('.imageViewerViewport').get(0);
displayReferenceLines(domElement);
StudyPrefetcher.getInstance().prefetch();
@@ -161,15 +161,15 @@ const thumbnailDragHandler = event => {
}
// Remove any current faded effects on viewports
$('.imageViewerViewport canvas').removeClass('faded');
$('.viewportContainer canvas').removeClass('faded');
// Figure out what to do depending on what we're dragging over
const $viewportsDraggedOver = $(elemBelow).parents('.imageViewerViewport');
const $viewportsDraggedOver = $(elemBelow).parents('.viewportContainer');
if ($viewportsDraggedOver.length) {
// If we're dragging over a non-empty viewport, fade it and change the cursor style
$viewportsDraggedOver.find('canvas').not('.magnifyTool').addClass('faded');
document.body.style.cursor = 'copy';
} else if (elemBelow.classList.contains('imageViewerViewport') && elemBelow.classList.contains('empty')) {
} else if (elemBelow.classList.contains('viewportContainer') && elemBelow.classList.contains('empty')) {
// If we're dragging over an empty viewport, just change the cursor style
document.body.style.cursor = 'copy';
} else {
@@ -207,7 +207,7 @@ const thumbnailDragEndHandler = (event, data, handlers) => {
$('.imageThumbnailClone').remove();
// Remove any current faded effects on viewports
$('.imageViewerViewport canvas').removeClass('faded');
$('.viewportContainer canvas').removeClass('faded');
// If none exists, stop here
if (!elemBelow) {
@@ -218,12 +218,12 @@ const thumbnailDragEndHandler = (event, data, handlers) => {
elemBelow.classList.remove('faded');
let element;
const $viewportsDraggedOver = $(elemBelow).closest('.imageViewerViewport');
const $viewportsDraggedOver = $(elemBelow).closest('.viewportContainer');
if ($viewportsDraggedOver.length) {
// If we're dragging over a non-empty viewport, retrieve it
element = $viewportsDraggedOver.get(0);
} else if (elemBelow.classList.contains('imageViewerViewport') &&
} else if (elemBelow.classList.contains('viewportContainer') &&
elemBelow.classList.contains('empty')) {
// If we're dragging over an empty viewport, retrieve that instead
element = elemBelow;
@@ -238,7 +238,7 @@ const thumbnailDragEndHandler = (event, data, handlers) => {
}
// Get the dropped viewport index
const viewportIndex = $('.imageViewerViewport').index(element);
const viewportIndex = $('.viewportContainer').index(element);
// Rerender the viewport using the dragged thumbnail data
OHIF.viewerbase.layoutManager.rerenderViewportWithNewDisplaySet(viewportIndex, data);
@@ -327,7 +327,7 @@ const isStackScrollLinkingDisabled = () => {
let linkableViewportsCount = 0;
// Its called everytime active viewport and/or layout change
Session.get('viewportActivated');
Session.get('activeViewport');
Session.get('LayoutManagerUpdated');
const synchronizer = OHIF.viewer.stackImagePositionOffsetSynchronizer;