Files
ohif-viewer/extensions/dicom-microscopy/src/tools/viewerManager.js
2025-03-07 13:47:44 +07:00

465 lines
14 KiB
JavaScript

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;