init
This commit is contained in:
464
extensions/dicom-microscopy/src/tools/viewerManager.js
Normal file
464
extensions/dicom-microscopy/src/tools/viewerManager.js
Normal file
@@ -0,0 +1,464 @@
|
||||
import coordinateFormatScoord3d2Geometry from '../utils/coordinateFormatScoord3d2Geometry';
|
||||
import styles from '../utils/styles';
|
||||
|
||||
import { PubSubService } from '@ohif/core';
|
||||
|
||||
// Events from the third-party viewer
|
||||
const ApiEvents = {
|
||||
/** Triggered when a ROI was added. */
|
||||
ROI_ADDED: 'dicommicroscopyviewer_roi_added',
|
||||
/** Triggered when a ROI was modified. */
|
||||
ROI_MODIFIED: 'dicommicroscopyviewer_roi_modified',
|
||||
/** Triggered when a ROI was removed. */
|
||||
ROI_REMOVED: 'dicommicroscopyviewer_roi_removed',
|
||||
/** Triggered when a ROI was drawn. */
|
||||
ROI_DRAWN: `dicommicroscopyviewer_roi_drawn`,
|
||||
/** Triggered when a ROI was selected. */
|
||||
ROI_SELECTED: `dicommicroscopyviewer_roi_selected`,
|
||||
/** Triggered when a viewport move has started. */
|
||||
MOVE_STARTED: `dicommicroscopyviewer_move_started`,
|
||||
/** Triggered when a viewport move has ended. */
|
||||
MOVE_ENDED: `dicommicroscopyviewer_move_ended`,
|
||||
/** Triggered when a loading of data has started. */
|
||||
LOADING_STARTED: `dicommicroscopyviewer_loading_started`,
|
||||
/** Triggered when a loading of data has ended. */
|
||||
LOADING_ENDED: `dicommicroscopyviewer_loading_ended`,
|
||||
/** Triggered when an error occurs during loading of data. */
|
||||
LOADING_ERROR: `dicommicroscopyviewer_loading_error`,
|
||||
/* Triggered when the loading of an image tile has started. */
|
||||
FRAME_LOADING_STARTED: `dicommicroscopyviewer_frame_loading_started`,
|
||||
/* Triggered when the loading of an image tile has ended. */
|
||||
FRAME_LOADING_ENDED: `dicommicroscopyviewer_frame_loading_ended`,
|
||||
/* Triggered when the error occurs during loading of an image tile. */
|
||||
FRAME_LOADING_ERROR: `dicommicroscopyviewer_frame_loading_ended`,
|
||||
};
|
||||
|
||||
const EVENTS = {
|
||||
ADDED: 'added',
|
||||
MODIFIED: 'modified',
|
||||
REMOVED: 'removed',
|
||||
UPDATED: 'updated',
|
||||
SELECTED: 'selected',
|
||||
};
|
||||
|
||||
/**
|
||||
* ViewerManager encapsulates the complexity of the third-party viewer and
|
||||
* expose only the features/behaviors that are relevant to the application
|
||||
*/
|
||||
class ViewerManager extends PubSubService {
|
||||
constructor(viewer, viewportId, container, studyInstanceUID, seriesInstanceUID) {
|
||||
super(EVENTS);
|
||||
this.viewer = viewer;
|
||||
this.viewportId = viewportId;
|
||||
this.container = container;
|
||||
this.studyInstanceUID = studyInstanceUID;
|
||||
this.seriesInstanceUID = seriesInstanceUID;
|
||||
|
||||
this.onRoiAdded = this.roiAddedHandler.bind(this);
|
||||
this.onRoiModified = this.roiModifiedHandler.bind(this);
|
||||
this.onRoiRemoved = this.roiRemovedHandler.bind(this);
|
||||
this.onRoiSelected = this.roiSelectedHandler.bind(this);
|
||||
this.contextMenuCallback = () => {};
|
||||
|
||||
// init symbols
|
||||
const symbols = Object.getOwnPropertySymbols(this.viewer);
|
||||
this._drawingSource = symbols.find(p => p.description === 'drawingSource');
|
||||
this._pyramid = symbols.find(p => p.description === 'pyramid');
|
||||
this._map = symbols.find(p => p.description === 'map');
|
||||
this._affine = symbols.find(p => p.description === 'affine');
|
||||
|
||||
this.registerEvents();
|
||||
this.activateDefaultInteractions();
|
||||
}
|
||||
|
||||
addContextMenuCallback(callback) {
|
||||
this.contextMenuCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys this managed viewer instance, clearing all the event handlers
|
||||
*/
|
||||
destroy() {
|
||||
this.unregisterEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* This is to overrides the _broadcastEvent method of PubSubService and always
|
||||
* send the ROI graphic object and this managed viewer instance.
|
||||
* Due to the way that PubSubService is written, the same name override of the
|
||||
* function doesn't work.
|
||||
*
|
||||
* @param {String} key key Subscription key
|
||||
* @param {Object} roiGraphic ROI graphic object created by the third-party API
|
||||
*/
|
||||
publish(key, roiGraphic) {
|
||||
this._broadcastEvent(key, {
|
||||
roiGraphic,
|
||||
managedViewer: this,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers all the relevant event handlers for the third-party API
|
||||
*/
|
||||
registerEvents() {
|
||||
this.container.addEventListener(ApiEvents.ROI_ADDED, this.onRoiAdded);
|
||||
this.container.addEventListener(ApiEvents.ROI_MODIFIED, this.onRoiModified);
|
||||
this.container.addEventListener(ApiEvents.ROI_REMOVED, this.onRoiRemoved);
|
||||
this.container.addEventListener(ApiEvents.ROI_SELECTED, this.onRoiSelected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all the relevant event handlers for the third-party API
|
||||
*/
|
||||
unregisterEvents() {
|
||||
this.container.removeEventListener(ApiEvents.ROI_ADDED, this.onRoiAdded);
|
||||
this.container.removeEventListener(ApiEvents.ROI_MODIFIED, this.onRoiModified);
|
||||
this.container.removeEventListener(ApiEvents.ROI_REMOVED, this.onRoiRemoved);
|
||||
this.container.removeEventListener(ApiEvents.ROI_SELECTED, this.onRoiSelected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the ROI_ADDED event triggered by the third-party API
|
||||
*
|
||||
* @param {Event} event Event triggered by the third-party API
|
||||
*/
|
||||
roiAddedHandler(event) {
|
||||
const roiGraphic = event.detail.payload;
|
||||
this.publish(EVENTS.ADDED, roiGraphic);
|
||||
this.publish(EVENTS.UPDATED, roiGraphic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the ROI_MODIFIED event triggered by the third-party API
|
||||
*
|
||||
* @param {Event} event Event triggered by the third-party API
|
||||
*/
|
||||
roiModifiedHandler(event) {
|
||||
const roiGraphic = event.detail.payload;
|
||||
this.publish(EVENTS.MODIFIED, roiGraphic);
|
||||
this.publish(EVENTS.UPDATED, roiGraphic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the ROI_REMOVED event triggered by the third-party API
|
||||
*
|
||||
* @param {Event} event Event triggered by the third-party API
|
||||
*/
|
||||
roiRemovedHandler(event) {
|
||||
const roiGraphic = event.detail.payload;
|
||||
this.publish(EVENTS.REMOVED, roiGraphic);
|
||||
this.publish(EVENTS.UPDATED, roiGraphic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the ROI_SELECTED event triggered by the third-party API
|
||||
*
|
||||
* @param {Event} event Event triggered by the third-party API
|
||||
*/
|
||||
roiSelectedHandler(event) {
|
||||
const roiGraphic = event.detail.payload;
|
||||
this.publish(EVENTS.SELECTED, roiGraphic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the given callback operation without triggering any events for this
|
||||
* instance, so subscribers will not be affected
|
||||
*
|
||||
* @param {Function} callback Callback that will run sinlently
|
||||
*/
|
||||
runSilently(callback) {
|
||||
this.unregisterEvents();
|
||||
callback();
|
||||
this.registerEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all the ROI graphics from the third-party API
|
||||
*/
|
||||
clearRoiGraphics() {
|
||||
this.runSilently(() => this.viewer.removeAllROIs());
|
||||
}
|
||||
|
||||
showROIs() {
|
||||
this.viewer.showROIs();
|
||||
}
|
||||
|
||||
hideROIs() {
|
||||
this.viewer.hideROIs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given ROI graphic into the third-party API
|
||||
*
|
||||
* @param {Object} roiGraphic ROI graphic object to be added
|
||||
*/
|
||||
addRoiGraphic(roiGraphic) {
|
||||
this.runSilently(() => this.viewer.addROI(roiGraphic, styles.default));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given ROI graphic into the third-party API, and also add a label.
|
||||
* Used for importing from SR.
|
||||
*
|
||||
* @param {Object} roiGraphic ROI graphic object to be added.
|
||||
* @param {String} label The label of the annotation.
|
||||
*/
|
||||
addRoiGraphicWithLabel(roiGraphic, label) {
|
||||
// NOTE: Dicom Microscopy Viewer will override styles for "Text" evaluations
|
||||
// to hide all other geometries, we are not going to use its label.
|
||||
// if (label) {
|
||||
// if (!roiGraphic.properties) roiGraphic.properties = {};
|
||||
// roiGraphic.properties.label = label;
|
||||
// }
|
||||
this.runSilently(() => this.viewer.addROI(roiGraphic, styles.default));
|
||||
|
||||
this._broadcastEvent(EVENTS.ADDED, {
|
||||
roiGraphic,
|
||||
managedViewer: this,
|
||||
label,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets ROI style
|
||||
*
|
||||
* @param {String} uid ROI graphic UID to be styled
|
||||
* @param {object} styleOptions - Style options
|
||||
* @param {object} styleOptions.stroke - Style options for the outline of the geometry
|
||||
* @param {number[]} styleOptions.stroke.color - RGBA color of the outline
|
||||
* @param {number} styleOptions.stroke.width - Width of the outline
|
||||
* @param {object} styleOptions.fill - Style options for body the geometry
|
||||
* @param {number[]} styleOptions.fill.color - RGBA color of the body
|
||||
* @param {object} styleOptions.image - Style options for image
|
||||
*/
|
||||
setROIStyle(uid, styleOptions) {
|
||||
this.viewer.setROIStyle(uid, styleOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the ROI graphic with the given UID from the third-party API
|
||||
*
|
||||
* @param {String} uid ROI graphic UID to be removed
|
||||
*/
|
||||
removeRoiGraphic(uid) {
|
||||
this.viewer.removeROI(uid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update properties of regions of interest.
|
||||
*
|
||||
* @param {object} roi - ROI to be updated
|
||||
* @param {string} roi.uid - Unique identifier of the region of interest
|
||||
* @param {object} roi.properties - ROI properties
|
||||
* @returns {void}
|
||||
*/
|
||||
updateROIProperties({ uid, properties }) {
|
||||
this.viewer.updateROI({ uid, properties });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles overview map
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
toggleOverviewMap() {
|
||||
this.viewer.toggleOverviewMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the viewer default interactions
|
||||
* @returns {void}
|
||||
*/
|
||||
activateDefaultInteractions() {
|
||||
/** Disable browser's native context menu inside the canvas */
|
||||
document.querySelector('.DicomMicroscopyViewer').addEventListener(
|
||||
'contextmenu',
|
||||
event => {
|
||||
event.preventDefault();
|
||||
// comment out when context menu for microscopy is enabled
|
||||
// if (typeof this.contextMenuCallback === 'function') {
|
||||
// this.contextMenuCallback(event);
|
||||
// }
|
||||
},
|
||||
false
|
||||
);
|
||||
const defaultInteractions = [
|
||||
[
|
||||
'dragPan',
|
||||
{
|
||||
bindings: {
|
||||
mouseButtons: ['middle'],
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
'dragZoom',
|
||||
{
|
||||
bindings: {
|
||||
mouseButtons: ['right'],
|
||||
},
|
||||
},
|
||||
],
|
||||
['modify', {}],
|
||||
];
|
||||
this.activateInteractions(defaultInteractions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates interactions
|
||||
* @param {Array} interactions Interactions to be activated
|
||||
* @returns {void}
|
||||
*/
|
||||
activateInteractions(interactions) {
|
||||
const interactionsMap = {
|
||||
draw: activate => (activate ? 'activateDrawInteraction' : 'deactivateDrawInteraction'),
|
||||
modify: activate => (activate ? 'activateModifyInteraction' : 'deactivateModifyInteraction'),
|
||||
translate: activate =>
|
||||
activate ? 'activateTranslateInteraction' : 'deactivateTranslateInteraction',
|
||||
snap: activate => (activate ? 'activateSnapInteraction' : 'deactivateSnapInteraction'),
|
||||
dragPan: activate =>
|
||||
activate ? 'activateDragPanInteraction' : 'deactivateDragPanInteraction',
|
||||
dragZoom: activate =>
|
||||
activate ? 'activateDragZoomInteraction' : 'deactivateDragZoomInteraction',
|
||||
select: activate => (activate ? 'activateSelectInteraction' : 'deactivateSelectInteraction'),
|
||||
};
|
||||
|
||||
const availableInteractionsName = Object.keys(interactionsMap);
|
||||
availableInteractionsName.forEach(availableInteractionName => {
|
||||
const interaction = interactions.find(
|
||||
interaction => interaction[0] === availableInteractionName
|
||||
);
|
||||
if (!interaction) {
|
||||
const deactivateInteractionMethod = interactionsMap[availableInteractionName](false);
|
||||
this.viewer[deactivateInteractionMethod]();
|
||||
} else {
|
||||
const [name, config] = interaction;
|
||||
const activateInteractionMethod = interactionsMap[name](true);
|
||||
this.viewer[activateInteractionMethod](config);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Accesses the internals of third-party API and returns the OpenLayers Map
|
||||
*
|
||||
* @returns {Object} OpenLayers Map component instance
|
||||
*/
|
||||
_getMapView() {
|
||||
const map = this._getMap();
|
||||
return map.getView();
|
||||
}
|
||||
|
||||
_getMap() {
|
||||
const symbols = Object.getOwnPropertySymbols(this.viewer);
|
||||
const _map = symbols.find(s => String(s) === 'Symbol(map)');
|
||||
window['map'] = this.viewer[_map];
|
||||
return this.viewer[_map];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state for the OpenLayers View
|
||||
*
|
||||
* @returns {Object} Current view state
|
||||
*/
|
||||
getViewState() {
|
||||
const view = this._getMapView();
|
||||
return {
|
||||
center: view.getCenter(),
|
||||
resolution: view.getResolution(),
|
||||
zoom: view.getZoom(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current state for the OpenLayers View
|
||||
*
|
||||
* @param {Object} viewState View state to be applied
|
||||
*/
|
||||
setViewState(viewState) {
|
||||
const view = this._getMapView();
|
||||
|
||||
view.setZoom(viewState.zoom);
|
||||
view.setResolution(viewState.resolution);
|
||||
view.setCenter(viewState.center);
|
||||
}
|
||||
|
||||
setViewStateByExtent(roiAnnotation) {
|
||||
const coordinates = roiAnnotation.getCoordinates();
|
||||
|
||||
if (Array.isArray(coordinates[0]) && !coordinates[2]) {
|
||||
this._jumpToPolyline(coordinates);
|
||||
} else if (Array.isArray(coordinates[0])) {
|
||||
this._jumpToPolygonOrEllipse(coordinates);
|
||||
} else {
|
||||
this._jumpToPoint(coordinates);
|
||||
}
|
||||
}
|
||||
|
||||
_jumpToPoint(coord) {
|
||||
const pyramid = this.viewer[this._pyramid].metadata;
|
||||
|
||||
const mappedCoord = coordinateFormatScoord3d2Geometry(coord, pyramid);
|
||||
const view = this._getMapView();
|
||||
|
||||
view.setCenter(mappedCoord);
|
||||
}
|
||||
|
||||
_jumpToPolyline(coord) {
|
||||
const pyramid = this.viewer[this._pyramid].metadata;
|
||||
|
||||
const mappedCoord = coordinateFormatScoord3d2Geometry(coord, pyramid);
|
||||
const view = this._getMapView();
|
||||
|
||||
const x = mappedCoord[0];
|
||||
const y = mappedCoord[1];
|
||||
|
||||
const xab = (x[0] + y[0]) / 2;
|
||||
const yab = (x[1] + y[1]) / 2;
|
||||
const midpoint = [xab, yab];
|
||||
|
||||
view.setCenter(midpoint);
|
||||
}
|
||||
|
||||
_jumpToPolygonOrEllipse(coordinates) {
|
||||
const pyramid = this.viewer[this._pyramid].metadata;
|
||||
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let minY = Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
coordinates.forEach(coord => {
|
||||
let mappedCoord = coordinateFormatScoord3d2Geometry(coord, pyramid);
|
||||
|
||||
const [x, y] = mappedCoord;
|
||||
if (x < minX) {
|
||||
minX = x;
|
||||
} else if (x > maxX) {
|
||||
maxX = x;
|
||||
}
|
||||
|
||||
if (y < minY) {
|
||||
minY = y;
|
||||
} else if (y > maxY) {
|
||||
maxY = y;
|
||||
}
|
||||
});
|
||||
|
||||
const width = maxX - minX;
|
||||
const height = maxY - minY;
|
||||
|
||||
minX -= 0.5 * width;
|
||||
maxX += 0.5 * width;
|
||||
minY -= 0.5 * height;
|
||||
maxY += 0.5 * height;
|
||||
|
||||
const map = this._getMap();
|
||||
map.getView().fit([minX, minY, maxX, maxY], map.getSize());
|
||||
}
|
||||
}
|
||||
|
||||
export { EVENTS };
|
||||
|
||||
export default ViewerManager;
|
||||
Reference in New Issue
Block a user