465 lines
14 KiB
JavaScript
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;
|