feat(Thumbnail): Display set messages support & displaying of series inconsistencies in the thumbnail (#3499)
Co-authored-by: Igor Octaviano <igoroctaviano@gmail.com>
This commit is contained in:
1 parent
aedb9d1576
commit
5302e5b62b
31 files changed
+1126
-12
No files matched your search
@@ -317,6 +317,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
|
|||||||
numInstances: ds.numImageFrames,
|
numInstances: ds.numImageFrames,
|
||||||
countIcon: ds.countIcon,
|
countIcon: ds.countIcon,
|
||||||
StudyInstanceUID: ds.StudyInstanceUID,
|
StudyInstanceUID: ds.StudyInstanceUID,
|
||||||
|
messages: ds.messages,
|
||||||
componentType,
|
componentType,
|
||||||
imageSrc,
|
imageSrc,
|
||||||
dragData: {
|
dragData: {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import sortInstancesByPosition from '@ohif/core/src/utils/sortInstancesByPosition';
|
||||||
|
import { constructableModalities } from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||||
|
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
|
||||||
|
import checkMultiFrame from './utils/validations/checkMultiframe';
|
||||||
|
import checkSingleFrames from './utils/validations/checkSingleFrames';
|
||||||
|
/**
|
||||||
|
* Checks if a series is reconstructable to a 3D volume.
|
||||||
|
*
|
||||||
|
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
|
||||||
|
*/
|
||||||
|
export default function getDisplaySetMessages(
|
||||||
|
instances: Array<any>,
|
||||||
|
isReconstructable: boolean
|
||||||
|
): DisplaySetMessageList {
|
||||||
|
const messages = new DisplaySetMessageList();
|
||||||
|
if (!instances.length) {
|
||||||
|
messages.addMessage(DisplaySetMessage.CODES.NO_VALID_INSTANCES);
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstInstance = instances[0];
|
||||||
|
// Due to current requirements, LOCALIZER series doesn't have any messages
|
||||||
|
if (firstInstance.ImageType.includes('LOCALIZER')) {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Modality = firstInstance.Modality;
|
||||||
|
if (!constructableModalities.includes(Modality)) {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMultiframe = firstInstance.NumberOfFrames > 1;
|
||||||
|
// Can't reconstruct if all instances don't have the ImagePositionPatient.
|
||||||
|
if (
|
||||||
|
!isMultiframe &&
|
||||||
|
!instances.every(instance => instance.ImagePositionPatient)
|
||||||
|
) {
|
||||||
|
messages.addMessage(DisplaySetMessage.CODES.NO_POSITION_INFORMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedInstances = sortInstancesByPosition(instances);
|
||||||
|
|
||||||
|
isMultiframe
|
||||||
|
? checkMultiFrame(sortedInstances[0], messages)
|
||||||
|
: checkSingleFrames(sortedInstances, messages);
|
||||||
|
|
||||||
|
if (!isReconstructable) {
|
||||||
|
messages.addMessage(DisplaySetMessage.CODES.NOT_RECONSTRUCTABLE);
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import sopClassDictionary from '@ohif/core/src/utils/sopClassDictionary';
|
|||||||
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
||||||
import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||||
import { id } from './id';
|
import { id } from './id';
|
||||||
|
import getDisplaySetMessages from './getDisplaySetMessages';
|
||||||
|
|
||||||
const sopClassHandlerName = 'stack';
|
const sopClassHandlerName = 'stack';
|
||||||
|
|
||||||
@@ -18,8 +19,9 @@ const makeDisplaySet = instances => {
|
|||||||
value: isReconstructable,
|
value: isReconstructable,
|
||||||
averageSpacingBetweenFrames,
|
averageSpacingBetweenFrames,
|
||||||
} = isDisplaySetReconstructable(instances);
|
} = isDisplaySetReconstructable(instances);
|
||||||
|
|
||||||
// set appropriate attributes to image set...
|
// set appropriate attributes to image set...
|
||||||
|
const messages = getDisplaySetMessages(instances, isReconstructable);
|
||||||
|
|
||||||
imageSet.setAttributes({
|
imageSet.setAttributes({
|
||||||
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
|
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
|
||||||
SeriesDate: instance.SeriesDate,
|
SeriesDate: instance.SeriesDate,
|
||||||
@@ -36,6 +38,7 @@ const makeDisplaySet = instances => {
|
|||||||
numImageFrames: instances.length,
|
numImageFrames: instances.length,
|
||||||
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
||||||
isReconstructable,
|
isReconstructable,
|
||||||
|
messages,
|
||||||
averageSpacingBetweenFrames: averageSpacingBetweenFrames || null,
|
averageSpacingBetweenFrames: averageSpacingBetweenFrames || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { vec3 } from 'gl-matrix';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the scanAxisNormal based on a image orientation vector extract from a frame
|
||||||
|
* @param {*} imageOrientation
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function calculateScanAxisNormal(imageOrientation) {
|
||||||
|
const rowCosineVec = vec3.fromValues(
|
||||||
|
imageOrientation[0],
|
||||||
|
imageOrientation[1],
|
||||||
|
imageOrientation[2]
|
||||||
|
);
|
||||||
|
const colCosineVec = vec3.fromValues(
|
||||||
|
imageOrientation[3],
|
||||||
|
imageOrientation[4],
|
||||||
|
imageOrientation[5]
|
||||||
|
);
|
||||||
|
return vec3.cross(vec3.create(), rowCosineVec, colCosineVec);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import toNumber from '@ohif/core/src/utils/toNumber';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if all voxels in series images has same number of components (samplesPerPixel)
|
||||||
|
* @param {*} instances
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function areAllImageComponentsEqual(
|
||||||
|
instances: Array<any>
|
||||||
|
): boolean {
|
||||||
|
if (!instances?.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const firstImage = instances[0];
|
||||||
|
const firstImageSamplesPerPixel = toNumber(firstImage.SamplesPerPixel);
|
||||||
|
|
||||||
|
for (let i = 1; i < instances.length; i++) {
|
||||||
|
const instance = instances[i];
|
||||||
|
const { SamplesPerPixel } = instance;
|
||||||
|
|
||||||
|
if (SamplesPerPixel !== firstImageSamplesPerPixel) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import toNumber from '@ohif/core/src/utils/toNumber';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the frames in a series has different dimensions
|
||||||
|
* @param {*} instances
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function areAllImageDimensionsEqual(
|
||||||
|
instances: Array<any>
|
||||||
|
): boolean {
|
||||||
|
if (!instances?.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const firstImage = instances[0];
|
||||||
|
const firstImageRows = toNumber(firstImage.Rows);
|
||||||
|
const firstImageColumns = toNumber(firstImage.Columns);
|
||||||
|
|
||||||
|
for (let i = 1; i < instances.length; i++) {
|
||||||
|
const instance = instances[i];
|
||||||
|
const { Rows, Columns } = instance;
|
||||||
|
|
||||||
|
if (Rows !== firstImageRows || Columns !== firstImageColumns) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import toNumber from '@ohif/core/src/utils/toNumber';
|
||||||
|
import { _isSameOrientation } from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check is the series has frames with different orientations
|
||||||
|
* @param {*} instances
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function areAllImageOrientationsEqual(
|
||||||
|
instances: Array<any>
|
||||||
|
): boolean {
|
||||||
|
if (!instances?.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const firstImage = instances[0];
|
||||||
|
const firstImageOrientationPatient = toNumber(
|
||||||
|
firstImage.ImageOrientationPatient
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 1; i < instances.length; i++) {
|
||||||
|
const instance = instances[i];
|
||||||
|
const imageOrientationPatient = toNumber(instance.ImageOrientationPatient);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!_isSameOrientation(imageOrientationPatient, firstImageOrientationPatient)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { vec3 } from 'gl-matrix';
|
||||||
|
import toNumber from '@ohif/core/src/utils/toNumber';
|
||||||
|
import { _getPerpendicularDistance } from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||||
|
import calculateScanAxisNormal from '../calculateScanAxisNormal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if there is a position shift between consecutive frames
|
||||||
|
* @param {*} previousPosition
|
||||||
|
* @param {*} actualPosition
|
||||||
|
* @param {*} scanAxisNormal
|
||||||
|
* @param {*} averageSpacingBetweenFrames
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function _checkSeriesPositionShift(
|
||||||
|
previousPosition,
|
||||||
|
actualPosition,
|
||||||
|
scanAxisNormal,
|
||||||
|
averageSpacingBetweenFrames
|
||||||
|
) {
|
||||||
|
// predicted position should be the previous position added by the multiplication
|
||||||
|
// of the scanAxisNormal and the average spacing between frames
|
||||||
|
const predictedPosition = vec3.scaleAndAdd(
|
||||||
|
vec3.create(),
|
||||||
|
previousPosition,
|
||||||
|
scanAxisNormal,
|
||||||
|
averageSpacingBetweenFrames
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
vec3.distance(actualPosition, predictedPosition) >
|
||||||
|
averageSpacingBetweenFrames
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a series has position shifts between consecutive frames
|
||||||
|
* @param {*} instances
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export default function areAllImagePositionsEqual(
|
||||||
|
instances: Array<any>
|
||||||
|
): boolean {
|
||||||
|
if (!instances?.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const firstImageOrientationPatient = toNumber(
|
||||||
|
instances[0].ImageOrientationPatient
|
||||||
|
);
|
||||||
|
const scanAxisNormal = calculateScanAxisNormal(firstImageOrientationPatient);
|
||||||
|
const firstImagePositionPatient = toNumber(instances[0].ImagePositionPatient);
|
||||||
|
const lastIpp = toNumber(
|
||||||
|
instances[instances.length - 1].ImagePositionPatient
|
||||||
|
);
|
||||||
|
|
||||||
|
const averageSpacingBetweenFrames =
|
||||||
|
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||||
|
(instances.length - 1);
|
||||||
|
|
||||||
|
let previousImagePositionPatient = firstImagePositionPatient;
|
||||||
|
for (let i = 1; i < instances.length; i++) {
|
||||||
|
const instance = instances[i];
|
||||||
|
const imagePositionPatient = toNumber(instance.ImagePositionPatient);
|
||||||
|
|
||||||
|
if (
|
||||||
|
_checkSeriesPositionShift(
|
||||||
|
previousImagePositionPatient,
|
||||||
|
imagePositionPatient,
|
||||||
|
scanAxisNormal,
|
||||||
|
averageSpacingBetweenFrames
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
previousImagePositionPatient = imagePositionPatient;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
_getPerpendicularDistance,
|
||||||
|
_getSpacingIssue,
|
||||||
|
reconstructionIssues,
|
||||||
|
} from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||||
|
import { DisplaySetMessage } from '@ohif/core';
|
||||||
|
import toNumber from '@ohif/core/src/utils/toNumber';
|
||||||
|
import { DisplaySetMessageList } from '@ohif/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if series has spacing issues
|
||||||
|
* @param {*} instances
|
||||||
|
* @param {*} warnings
|
||||||
|
*/
|
||||||
|
export default function areAllImageSpacingEqual(
|
||||||
|
instances: Array<any>,
|
||||||
|
messages: DisplaySetMessageList
|
||||||
|
): void {
|
||||||
|
if (!instances?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const firstImagePositionPatient = toNumber(instances[0].ImagePositionPatient);
|
||||||
|
const lastIpp = toNumber(
|
||||||
|
instances[instances.length - 1].ImagePositionPatient
|
||||||
|
);
|
||||||
|
|
||||||
|
const averageSpacingBetweenFrames =
|
||||||
|
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||||
|
(instances.length - 1);
|
||||||
|
|
||||||
|
let previousImagePositionPatient = firstImagePositionPatient;
|
||||||
|
|
||||||
|
const issuesFound = [];
|
||||||
|
for (let i = 1; i < instances.length; i++) {
|
||||||
|
const instance = instances[i];
|
||||||
|
const imagePositionPatient = toNumber(instance.ImagePositionPatient);
|
||||||
|
|
||||||
|
const spacingBetweenFrames = _getPerpendicularDistance(
|
||||||
|
imagePositionPatient,
|
||||||
|
previousImagePositionPatient
|
||||||
|
);
|
||||||
|
|
||||||
|
const spacingIssue = _getSpacingIssue(
|
||||||
|
spacingBetweenFrames,
|
||||||
|
averageSpacingBetweenFrames
|
||||||
|
);
|
||||||
|
|
||||||
|
if (spacingIssue) {
|
||||||
|
const issue = spacingIssue.issue;
|
||||||
|
|
||||||
|
// avoid multiple warning of the same thing
|
||||||
|
if (!issuesFound.includes(issue)) {
|
||||||
|
issuesFound.push(issue);
|
||||||
|
if (issue === reconstructionIssues.MISSING_FRAMES) {
|
||||||
|
messages.addMessage(DisplaySetMessage.CODES.MISSING_FRAMES);
|
||||||
|
} else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
|
||||||
|
messages.addMessage(DisplaySetMessage.CODES.IRREGULAR_SPACING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we just want to find issues not how many
|
||||||
|
if (issuesFound.length > 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previousImagePositionPatient = imagePositionPatient;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
hasPixelMeasurements,
|
||||||
|
hasOrientation,
|
||||||
|
hasPosition,
|
||||||
|
} from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||||
|
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check various multi frame issues. It calls OHIF core functions
|
||||||
|
* @param {*} multiFrameInstance
|
||||||
|
* @param {*} warnings
|
||||||
|
*/
|
||||||
|
export default function checkMultiFrame(
|
||||||
|
multiFrameInstance,
|
||||||
|
messages: DisplaySetMessageList
|
||||||
|
): void {
|
||||||
|
if (!hasPixelMeasurements(multiFrameInstance)) {
|
||||||
|
messages.addMessage(
|
||||||
|
DisplaySetMessage.CODES.MULTIFRAME_NO_PIXEL_MEASUREMENTS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasOrientation(multiFrameInstance)) {
|
||||||
|
messages.addMessage(DisplaySetMessage.CODES.MULTIFRAME_NO_ORIENTATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPosition(multiFrameInstance)) {
|
||||||
|
messages.addMessage(
|
||||||
|
DisplaySetMessage.CODES.MULTIFRAME_NO_POSITION_INFORMATION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import areAllImageDimensionsEqual from './areAllImageDimensionsEqual';
|
||||||
|
import areAllImageComponentsEqual from './areAllImageComponentsEqual';
|
||||||
|
import areAllImageOrientationsEqual from './areAllImageOrientationsEqual';
|
||||||
|
import areAllImagePositionsEqual from './areAllImagePositionsEqual';
|
||||||
|
import areAllImageSpacingEqual from './areAllImageSpacingEqual';
|
||||||
|
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs various checks in a single frame series
|
||||||
|
* @param {*} instances
|
||||||
|
* @param {*} warnings
|
||||||
|
*/
|
||||||
|
export default function checkSingleFrames(
|
||||||
|
instances: Array<any>,
|
||||||
|
messages: DisplaySetMessageList
|
||||||
|
): void {
|
||||||
|
if (instances.length > 2) {
|
||||||
|
if (!areAllImageDimensionsEqual(instances)) {
|
||||||
|
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_DIMENSIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!areAllImageComponentsEqual(instances)) {
|
||||||
|
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_COMPONENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!areAllImageOrientationsEqual(instances)) {
|
||||||
|
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_ORIENTATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!areAllImagePositionsEqual(instances)) {
|
||||||
|
messages.addMessage(
|
||||||
|
DisplaySetMessage.CODES.INCONSISTENT_POSITION_INFORMATION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
areAllImageSpacingEqual(instances, messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
@@ -449,6 +449,7 @@ function _mapDisplaySets(
|
|||||||
seriesDate: formatDate(ds.SeriesDate),
|
seriesDate: formatDate(ds.SeriesDate),
|
||||||
numInstances: ds.numImageFrames,
|
numInstances: ds.numImageFrames,
|
||||||
countIcon: ds.countIcon,
|
countIcon: ds.countIcon,
|
||||||
|
messages: ds.messages,
|
||||||
StudyInstanceUID: ds.StudyInstanceUID,
|
StudyInstanceUID: ds.StudyInstanceUID,
|
||||||
componentType,
|
componentType,
|
||||||
imageSrc,
|
imageSrc,
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ describe('Top level exports', () => {
|
|||||||
'UserAuthenticationService',
|
'UserAuthenticationService',
|
||||||
'IWebApiDataSource',
|
'IWebApiDataSource',
|
||||||
'DicomMetadataStore',
|
'DicomMetadataStore',
|
||||||
|
'DisplaySetMessage',
|
||||||
|
'DisplaySetMessageList',
|
||||||
'pubSubServiceInterface',
|
'pubSubServiceInterface',
|
||||||
'PubSubService',
|
'PubSubService',
|
||||||
'PanelService',
|
'PanelService',
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ import {
|
|||||||
PanelService,
|
PanelService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DisplaySetMessage,
|
||||||
|
DisplaySetMessageList,
|
||||||
|
} from './services/DisplaySetService';
|
||||||
|
|
||||||
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
||||||
|
|
||||||
const hotkeys = {
|
const hotkeys = {
|
||||||
@@ -107,6 +112,8 @@ export {
|
|||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIViewportDialogService,
|
UIViewportDialogService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
|
DisplaySetMessage,
|
||||||
|
DisplaySetMessageList,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
ToolbarService,
|
ToolbarService,
|
||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Defines a displaySet message, that could be any pf the potential problems of a displaySet
|
||||||
|
*/
|
||||||
|
class DisplaySetMessage {
|
||||||
|
id: number;
|
||||||
|
static CODES = {
|
||||||
|
NO_VALID_INSTANCES: 1,
|
||||||
|
NO_POSITION_INFORMATION: 2,
|
||||||
|
NOT_RECONSTRUCTABLE: 3,
|
||||||
|
MULTIFRAME_NO_PIXEL_MEASUREMENTS: 4,
|
||||||
|
MULTIFRAME_NO_ORIENTATION: 5,
|
||||||
|
MULTIFRAME_NO_POSITION_INFORMATION: 6,
|
||||||
|
MISSING_FRAMES: 7,
|
||||||
|
IRREGULAR_SPACING: 8,
|
||||||
|
INCONSISTENT_DIMENSIONS: 9,
|
||||||
|
INCONSISTENT_COMPONENTS: 10,
|
||||||
|
INCONSISTENT_ORIENTATIONS: 11,
|
||||||
|
INCONSISTENT_POSITION_INFORMATION: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(id: number) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Defines a list of displaySet messages
|
||||||
|
*/
|
||||||
|
class DisplaySetMessageList {
|
||||||
|
messages = [];
|
||||||
|
|
||||||
|
public addMessage(messageId: number): void {
|
||||||
|
const message = new DisplaySetMessage(messageId);
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public size(): number {
|
||||||
|
return this.messages.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public includesMessage(messageId: number): boolean {
|
||||||
|
return this.messages.some(message => message.id === messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public includesAllMessages(messageIdList: number[]): boolean {
|
||||||
|
return messageIdList.every(messageId => this.include(messageId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DisplaySetMessage, DisplaySetMessageList };
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
import DisplaySetService from './DisplaySetService';
|
import DisplaySetService from './DisplaySetService';
|
||||||
|
import { DisplaySetMessage, DisplaySetMessageList } from './DisplaySetMessage';
|
||||||
|
|
||||||
export default DisplaySetService;
|
export default DisplaySetService;
|
||||||
|
export { DisplaySetMessage, DisplaySetMessageList };
|
||||||
@@ -54,8 +54,8 @@ function hasPixelMeasurements(multiFrameInstance) {
|
|||||||
Boolean(sharedSequence?.PixelMeasuresSequence) ||
|
Boolean(sharedSequence?.PixelMeasuresSequence) ||
|
||||||
Boolean(
|
Boolean(
|
||||||
multiFrameInstance.PixelSpacing &&
|
multiFrameInstance.PixelSpacing &&
|
||||||
(multiFrameInstance.SliceThickness ||
|
(multiFrameInstance.SliceThickness ||
|
||||||
multiFrameInstance.SpacingBetweenFrames)
|
multiFrameInstance.SpacingBetweenFrames)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -70,8 +70,8 @@ function hasOrientation(multiFrameInstance) {
|
|||||||
Boolean(perFrameSequence?.PlaneOrientationSequence) ||
|
Boolean(perFrameSequence?.PlaneOrientationSequence) ||
|
||||||
Boolean(
|
Boolean(
|
||||||
multiFrameInstance.ImageOrientationPatient ||
|
multiFrameInstance.ImageOrientationPatient ||
|
||||||
multiFrameInstance.DetectorInformationSequence?.[0]
|
multiFrameInstance.DetectorInformationSequence?.[0]
|
||||||
?.ImageOrientationPatient
|
?.ImageOrientationPatient
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -85,8 +85,8 @@ function hasPosition(multiFrameInstance) {
|
|||||||
Boolean(perFrameSequence?.CTPositionSequence) ||
|
Boolean(perFrameSequence?.CTPositionSequence) ||
|
||||||
Boolean(
|
Boolean(
|
||||||
multiFrameInstance.ImagePositionPatient ||
|
multiFrameInstance.ImagePositionPatient ||
|
||||||
multiFrameInstance.DetectorInformationSequence?.[0]
|
multiFrameInstance.DetectorInformationSequence?.[0]
|
||||||
?.ImagePositionPatient
|
?.ImagePositionPatient
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -161,6 +161,7 @@ function processSingleframe(instances) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let missingFrames = 0;
|
let missingFrames = 0;
|
||||||
|
let averageSpacingBetweenFrames;
|
||||||
|
|
||||||
// Check if frame spacing is approximately equal within a spacingTolerance.
|
// Check if frame spacing is approximately equal within a spacingTolerance.
|
||||||
// If spacing is on a uniform grid but we are missing frames,
|
// If spacing is on a uniform grid but we are missing frames,
|
||||||
@@ -175,7 +176,7 @@ function processSingleframe(instances) {
|
|||||||
return { value: false };
|
return { value: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const averageSpacingBetweenFrames =
|
averageSpacingBetweenFrames =
|
||||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||||
(instances.length - 1);
|
(instances.length - 1);
|
||||||
|
|
||||||
@@ -209,7 +210,7 @@ function processSingleframe(instances) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { value: true, missingFrames };
|
return { value: true, averageSpacingBetweenFrames };
|
||||||
}
|
}
|
||||||
|
|
||||||
function _isSameOrientation(iop1, iop2) {
|
function _isSameOrientation(iop1, iop2) {
|
||||||
@@ -220,7 +221,10 @@ function _isSameOrientation(iop1, iop2) {
|
|||||||
return (
|
return (
|
||||||
Math.abs(iop1[0] - iop2[0]) < iopTolerance &&
|
Math.abs(iop1[0] - iop2[0]) < iopTolerance &&
|
||||||
Math.abs(iop1[1] - iop2[1]) < iopTolerance &&
|
Math.abs(iop1[1] - iop2[1]) < iopTolerance &&
|
||||||
Math.abs(iop1[2] - iop2[2]) < iopTolerance
|
Math.abs(iop1[2] - iop2[2]) < iopTolerance &&
|
||||||
|
Math.abs(iop1[3] - iop2[3]) < iopTolerance &&
|
||||||
|
Math.abs(iop1[4] - iop2[4]) < iopTolerance &&
|
||||||
|
Math.abs(iop1[5] - iop2[5]) < iopTolerance
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,8 +264,8 @@ function _getSpacingIssue(spacing, averageSpacing) {
|
|||||||
function _getPerpendicularDistance(a, b) {
|
function _getPerpendicularDistance(a, b) {
|
||||||
return Math.sqrt(
|
return Math.sqrt(
|
||||||
Math.pow(a[0] - b[0], 2) +
|
Math.pow(a[0] - b[0], 2) +
|
||||||
Math.pow(a[1] - b[1], 2) +
|
Math.pow(a[1] - b[1], 2) +
|
||||||
Math.pow(a[2] - b[2], 2)
|
Math.pow(a[2] - b[2], 2)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,3 +274,15 @@ const reconstructionIssues = {
|
|||||||
MISSING_FRAMES: 'missingframes',
|
MISSING_FRAMES: 'missingframes',
|
||||||
IRREGULAR_SPACING: 'irregularspacing',
|
IRREGULAR_SPACING: 'irregularspacing',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
hasPixelMeasurements,
|
||||||
|
hasOrientation,
|
||||||
|
hasPosition,
|
||||||
|
isNMReconstructable,
|
||||||
|
_isSameOrientation,
|
||||||
|
_getSpacingIssue,
|
||||||
|
_getPerpendicularDistance,
|
||||||
|
reconstructionIssues,
|
||||||
|
constructableModalities,
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"1": "No valid instances found in series.",
|
||||||
|
"2": "DisplaySet has missing position information.",
|
||||||
|
"3": "DisplaySet is not a reconstructable 3D volume.",
|
||||||
|
"4": "Multi frame displaySet don't have pixel measurement information.",
|
||||||
|
"5": "Multi frame displaySet don't have orientation information.",
|
||||||
|
"6": "Multi frame displaySet don't have position information.",
|
||||||
|
"7": "DisplaySet has missing frames.",
|
||||||
|
"8": "DisplaySet has irregular spacing.",
|
||||||
|
"9": "DisplaySet has inconsistent dimensions between frames.",
|
||||||
|
"10": "DisplaySet has frames with inconsistent number of components.",
|
||||||
|
"11": "DisplaySet has frames with inconsistent orientations.",
|
||||||
|
"12": "DisplaySet has inconsistent position information."
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import StudyBrowser from './StudyBrowser.json';
|
|||||||
import StudyList from './StudyList.json';
|
import StudyList from './StudyList.json';
|
||||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
import ViewportDownloadForm from './ViewportDownloadForm.json';
|
||||||
|
import Messages from './Messages.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'en-US': {
|
'en-US': {
|
||||||
@@ -25,5 +26,6 @@ export default {
|
|||||||
StudyList,
|
StudyList,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
ViewportDownloadForm,
|
ViewportDownloadForm,
|
||||||
|
Messages,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"1": "Série sem imagens.",
|
||||||
|
"2": "Série nao possui informação de posição.",
|
||||||
|
"3": "Serie não é reconstruível.",
|
||||||
|
"4": "Série nulti frame não possui informação de medidas.",
|
||||||
|
"5": "Série multi frame não possui informação de orientação.",
|
||||||
|
"6": "Série multi frame não possui informação de posição.",
|
||||||
|
"7": "Série não possui algumas imagens.",
|
||||||
|
"8": "Série possui espaçamento irregular.",
|
||||||
|
"9": "Série possui dimensões inconsistentes entre frames.",
|
||||||
|
"10": "Série possui frames com componentes inconsistentes.",
|
||||||
|
"11": "Série possui frames com orientações inconsistentes.",
|
||||||
|
"12": "Série possui informação de posição inconsistentes."
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import DatePicker from './DatePicker.json';
|
|||||||
import Header from './Header.json';
|
import Header from './Header.json';
|
||||||
import UserPreferencesModal from './UserPreferencesModal.json';
|
import UserPreferencesModal from './UserPreferencesModal.json';
|
||||||
import MeasurementTable from './MeasurementTable.json';
|
import MeasurementTable from './MeasurementTable.json';
|
||||||
|
import Messages from './Messages.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'pt-BR': {
|
'pt-BR': {
|
||||||
@@ -17,5 +18,6 @@ export default {
|
|||||||
Header,
|
Header,
|
||||||
UserPreferencesModal,
|
UserPreferencesModal,
|
||||||
MeasurementTable,
|
MeasurementTable,
|
||||||
|
Messages,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="16" height="16.002" viewBox="0 0 16 16.002" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M16 7.863a8.118 8.118 0 0 1-8 8.138 7.881 7.881 0 0 1-8-7.86A8.118 8.118 0 0 1 8 0a7.882 7.882 0 0 1 8 7.862z" fill="#E3C33A"/>
|
||||||
|
<g stroke="#000" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke-width="2.5" d="M8 7.432V3.34"/>
|
||||||
|
<path d="M7.993 12.34a.178.178 0 0 0-.175.186c.004.1.085.178.184.179h0a.179.179 0 0 0 .175-.186.184.184 0 0 0-.177-.178h-.004" stroke-width="3"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 594 B |
+71
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import PortalTooltip from '../Tooltip/PortalTooltip';
|
||||||
|
import Icon from '../Icon';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a tooltip with a list of messages of a displaySet
|
||||||
|
* @param param0
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const DisplaySetMessageListTooltip = ({ messages, id }): React.ReactNode => {
|
||||||
|
const { t } = useTranslation('Messages');
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
if (messages.size()) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Icon
|
||||||
|
id={id}
|
||||||
|
onMouseOver={() => setIsOpen(true)}
|
||||||
|
onFocus={() => setIsOpen(true)}
|
||||||
|
onMouseOut={() => setIsOpen(false)}
|
||||||
|
onBlur={() => setIsOpen(false)}
|
||||||
|
name="status-alert-warning"
|
||||||
|
/>
|
||||||
|
<PortalTooltip
|
||||||
|
active={isOpen}
|
||||||
|
position="right"
|
||||||
|
arrow="center"
|
||||||
|
parent={`#${id}`}
|
||||||
|
>
|
||||||
|
<div className="bg-primary-dark border border-secondary-light text-white text-base rounded text-left max-w-40">
|
||||||
|
<div
|
||||||
|
className="break-normal text-base text-blue-300 font-bold"
|
||||||
|
style={{
|
||||||
|
marginLeft: '12px',
|
||||||
|
marginTop: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
DisplaySet Messages
|
||||||
|
</div>
|
||||||
|
<ol
|
||||||
|
style={{
|
||||||
|
marginLeft: '12px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{messages.messages.map((message, index) => (
|
||||||
|
<li
|
||||||
|
style={{
|
||||||
|
marginTop: '6px',
|
||||||
|
marginBottom: '6px',
|
||||||
|
}}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
{index + 1}. {t(message.id)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</PortalTooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
DisplaySetMessageListTooltip.propTypes = {
|
||||||
|
messages: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DisplaySetMessageListTooltip;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import DisplaySetMessageListTooltip from './DisplaySetMessageListTooltip';
|
||||||
|
export default DisplaySetMessageListTooltip;
|
||||||
@@ -42,6 +42,7 @@ import settings from './../../assets/icons/settings.svg';
|
|||||||
import sorting from './../../assets/icons/sorting.svg';
|
import sorting from './../../assets/icons/sorting.svg';
|
||||||
import sortingActiveDown from './../../assets/icons/sorting-active-down.svg';
|
import sortingActiveDown from './../../assets/icons/sorting-active-down.svg';
|
||||||
import sortingActiveUp from './../../assets/icons/sorting-active-up.svg';
|
import sortingActiveUp from './../../assets/icons/sorting-active-up.svg';
|
||||||
|
import statusAlertWarning from './../../assets/icons/status-alert-warning.svg';
|
||||||
import statusAlert from './../../assets/icons/status-alert.svg';
|
import statusAlert from './../../assets/icons/status-alert.svg';
|
||||||
import statusLocked from './../../assets/icons/status-locked.svg';
|
import statusLocked from './../../assets/icons/status-locked.svg';
|
||||||
import statusTracked from './../../assets/icons/status-tracked.svg';
|
import statusTracked from './../../assets/icons/status-tracked.svg';
|
||||||
@@ -181,6 +182,7 @@ const ICONS = {
|
|||||||
'sorting-active-down': sortingActiveDown,
|
'sorting-active-down': sortingActiveDown,
|
||||||
'sorting-active-up': sortingActiveUp,
|
'sorting-active-up': sortingActiveUp,
|
||||||
'status-alert': statusAlert,
|
'status-alert': statusAlert,
|
||||||
|
'status-alert-warning': statusAlertWarning,
|
||||||
'status-locked': statusLocked,
|
'status-locked': statusLocked,
|
||||||
'status-tracked': statusTracked,
|
'status-tracked': statusTracked,
|
||||||
'status-untracked': statusUntracked,
|
'status-untracked': statusUntracked,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import classnames from 'classnames';
|
|||||||
import { useDrag } from 'react-dnd';
|
import { useDrag } from 'react-dnd';
|
||||||
import Icon from '../Icon';
|
import Icon from '../Icon';
|
||||||
import { StringNumber } from '../../types';
|
import { StringNumber } from '../../types';
|
||||||
|
import DisplaySetMessageListTooltip from '../DisplaySetMessageListTooltip';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a thumbnail for a display set.
|
* Display a thumbnail for a display set.
|
||||||
@@ -17,6 +18,7 @@ const Thumbnail = ({
|
|||||||
seriesNumber,
|
seriesNumber,
|
||||||
numInstances,
|
numInstances,
|
||||||
countIcon,
|
countIcon,
|
||||||
|
messages,
|
||||||
dragData,
|
dragData,
|
||||||
isActive,
|
isActive,
|
||||||
onClick,
|
onClick,
|
||||||
@@ -78,6 +80,10 @@ const Thumbnail = ({
|
|||||||
<Icon name={countIcon || 'group-layers'} className="w-3 mr-2" />
|
<Icon name={countIcon || 'group-layers'} className="w-3 mr-2" />
|
||||||
{` ${numInstances}`}
|
{` ${numInstances}`}
|
||||||
</div>
|
</div>
|
||||||
|
<DisplaySetMessageListTooltip
|
||||||
|
messages={messages}
|
||||||
|
id={`display-set-tooltip-${seriesNumber}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-base text-white break-all">{description}</div>
|
<div className="text-base text-white break-all">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,6 +110,7 @@ Thumbnail.propTypes = {
|
|||||||
description: PropTypes.string.isRequired,
|
description: PropTypes.string.isRequired,
|
||||||
seriesNumber: StringNumber.isRequired,
|
seriesNumber: StringNumber.isRequired,
|
||||||
numInstances: PropTypes.number.isRequired,
|
numInstances: PropTypes.number.isRequired,
|
||||||
|
messages: PropTypes.object,
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
onDoubleClick: PropTypes.func.isRequired,
|
onDoubleClick: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const ThumbnailList = ({
|
|||||||
canReject,
|
canReject,
|
||||||
onReject,
|
onReject,
|
||||||
imageSrc,
|
imageSrc,
|
||||||
|
messages,
|
||||||
imageAltText,
|
imageAltText,
|
||||||
}) => {
|
}) => {
|
||||||
const isActive = activeDisplaySetInstanceUIDs.includes(
|
const isActive = activeDisplaySetInstanceUIDs.includes(
|
||||||
@@ -52,6 +53,7 @@ const ThumbnailList = ({
|
|||||||
countIcon={countIcon}
|
countIcon={countIcon}
|
||||||
imageSrc={imageSrc}
|
imageSrc={imageSrc}
|
||||||
imageAltText={imageAltText}
|
imageAltText={imageAltText}
|
||||||
|
messages={messages}
|
||||||
viewportIdentificator={viewportIdentificator}
|
viewportIdentificator={viewportIdentificator}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
onClick={() => onThumbnailClick(displaySetInstanceUID)}
|
onClick={() => onThumbnailClick(displaySetInstanceUID)}
|
||||||
@@ -72,6 +74,7 @@ const ThumbnailList = ({
|
|||||||
countIcon={countIcon}
|
countIcon={countIcon}
|
||||||
imageSrc={imageSrc}
|
imageSrc={imageSrc}
|
||||||
imageAltText={imageAltText}
|
imageAltText={imageAltText}
|
||||||
|
messages={messages}
|
||||||
viewportIdentificator={viewportIdentificator}
|
viewportIdentificator={viewportIdentificator}
|
||||||
isTracked={isTracked}
|
isTracked={isTracked}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
@@ -91,6 +94,7 @@ const ThumbnailList = ({
|
|||||||
dragData={dragData}
|
dragData={dragData}
|
||||||
modality={modality}
|
modality={modality}
|
||||||
modalityTooltip={_getModalityTooltip(modality)}
|
modalityTooltip={_getModalityTooltip(modality)}
|
||||||
|
messages={messages}
|
||||||
seriesDate={seriesDate}
|
seriesDate={seriesDate}
|
||||||
description={description}
|
description={description}
|
||||||
canReject={canReject}
|
canReject={canReject}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const ThumbnailNoImage = ({
|
|||||||
onDoubleClick,
|
onDoubleClick,
|
||||||
canReject,
|
canReject,
|
||||||
onReject,
|
onReject,
|
||||||
|
messages,
|
||||||
dragData,
|
dragData,
|
||||||
isActive,
|
isActive,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -56,6 +57,23 @@ const ThumbnailNoImage = ({
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<span className="ml-4 text-base text-blue-300">{seriesDate}</span>
|
<span className="ml-4 text-base text-blue-300">{seriesDate}</span>
|
||||||
|
{messages?.size() ? (
|
||||||
|
<div>
|
||||||
|
<Tooltip
|
||||||
|
position="left"
|
||||||
|
tight={true}
|
||||||
|
content={
|
||||||
|
<div className="text-left max-w-40">
|
||||||
|
{messages.thumbnailContents()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Icon name="notifications-warning" className="w-3 h-3" />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
{canReject && (
|
{canReject && (
|
||||||
@@ -96,6 +114,7 @@ ThumbnailNoImage.propTypes = {
|
|||||||
seriesDate: PropTypes.string.isRequired,
|
seriesDate: PropTypes.string.isRequired,
|
||||||
onClick: PropTypes.func.isRequired,
|
onClick: PropTypes.func.isRequired,
|
||||||
onDoubleClick: PropTypes.func.isRequired,
|
onDoubleClick: PropTypes.func.isRequired,
|
||||||
|
messages: PropTypes.object,
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const ThumbnailTracked = ({
|
|||||||
seriesNumber,
|
seriesNumber,
|
||||||
numInstances,
|
numInstances,
|
||||||
countIcon,
|
countIcon,
|
||||||
|
messages,
|
||||||
dragData,
|
dragData,
|
||||||
onClick,
|
onClick,
|
||||||
onDoubleClick,
|
onDoubleClick,
|
||||||
@@ -119,6 +120,7 @@ const ThumbnailTracked = ({
|
|||||||
dragData={dragData}
|
dragData={dragData}
|
||||||
description={description}
|
description={description}
|
||||||
seriesNumber={seriesNumber}
|
seriesNumber={seriesNumber}
|
||||||
|
messages={messages}
|
||||||
numInstances={numInstances}
|
numInstances={numInstances}
|
||||||
countIcon={countIcon}
|
countIcon={countIcon}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
@@ -153,6 +155,7 @@ ThumbnailTracked.propTypes = {
|
|||||||
onClickUntrack: PropTypes.func.isRequired,
|
onClickUntrack: PropTypes.func.isRequired,
|
||||||
viewportIdentificator: PropTypes.array,
|
viewportIdentificator: PropTypes.array,
|
||||||
isTracked: PropTypes.bool,
|
isTracked: PropTypes.bool,
|
||||||
|
messages: PropTypes.object,
|
||||||
isActive: PropTypes.bool.isRequired,
|
isActive: PropTypes.bool.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Card from './PortalTooltipCard';
|
||||||
|
|
||||||
|
const portalNodes = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A portal based tooltip component.
|
||||||
|
*
|
||||||
|
* This component has been repurposed and modified
|
||||||
|
* for OHIF usage: https://github.com/romainberger/react-portal-tooltip
|
||||||
|
*/
|
||||||
|
export default class PortalTooltip extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
parent: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
|
||||||
|
.isRequired,
|
||||||
|
active: PropTypes.bool,
|
||||||
|
group: PropTypes.string,
|
||||||
|
tooltipTimeout: PropTypes.number,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
active: false,
|
||||||
|
group: 'main',
|
||||||
|
tooltipTimeout: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
createPortal() {
|
||||||
|
portalNodes[this.props.group] = {
|
||||||
|
node: document.createElement('div'),
|
||||||
|
timeout: false,
|
||||||
|
};
|
||||||
|
portalNodes[this.props.group].node.className = 'ToolTipPortal';
|
||||||
|
document.body.appendChild(portalNodes[this.props.group].node);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPortal(props) {
|
||||||
|
if (!portalNodes[this.props.group]) {
|
||||||
|
this.createPortal();
|
||||||
|
}
|
||||||
|
let { parent, ...other } = props;
|
||||||
|
let parentEl =
|
||||||
|
typeof parent === 'string' ? document.querySelector(parent) : parent;
|
||||||
|
ReactDOM.render(
|
||||||
|
<Card parentEl={parentEl} {...other} />,
|
||||||
|
portalNodes[this.props.group].node
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderPortal(this.props);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (
|
||||||
|
(!portalNodes[this.props.group] && !nextProps.active) ||
|
||||||
|
(!this.props.active && !nextProps.active)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let props = { ...nextProps };
|
||||||
|
let newProps = { ...nextProps };
|
||||||
|
|
||||||
|
if (
|
||||||
|
portalNodes[this.props.group] &&
|
||||||
|
portalNodes[this.props.group].timeout
|
||||||
|
) {
|
||||||
|
clearTimeout(portalNodes[this.props.group].timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.active && !props.active) {
|
||||||
|
newProps.active = true;
|
||||||
|
portalNodes[this.props.group].timeout = setTimeout(() => {
|
||||||
|
props.active = false;
|
||||||
|
this.renderPortal(props);
|
||||||
|
}, this.props.tooltipTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderPortal(newProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (portalNodes[this.props.group]) {
|
||||||
|
ReactDOM.unmountComponentAtNode(portalNodes[this.props.group].node);
|
||||||
|
clearTimeout(portalNodes[this.props.group].timeout);
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.body.removeChild(portalNodes[this.props.group].node);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
portalNodes[this.props.group] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,413 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const FG_SIZE = 8;
|
||||||
|
const BG_SIZE = 9;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A portal based tooltip card component.
|
||||||
|
*
|
||||||
|
* This component has been repurposed and modified
|
||||||
|
* for OHIF usage: https://github.com/romainberger/react-portal-tooltip
|
||||||
|
*/
|
||||||
|
export default class PortalTooltipCard extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
active: PropTypes.bool,
|
||||||
|
position: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
||||||
|
arrow: PropTypes.oneOf([null, 'center', 'top', 'right', 'bottom', 'left']),
|
||||||
|
align: PropTypes.oneOf([null, 'center', 'right', 'left']),
|
||||||
|
style: PropTypes.object,
|
||||||
|
useHover: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
active: false,
|
||||||
|
position: 'right',
|
||||||
|
arrow: null,
|
||||||
|
align: null,
|
||||||
|
style: { style: {}, arrowStyle: {} },
|
||||||
|
useHover: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
hover: false,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
offscreenDifference = 0;
|
||||||
|
|
||||||
|
margin = 15;
|
||||||
|
|
||||||
|
defaultArrowStyle = {
|
||||||
|
color: '#090c29', // primary-dark
|
||||||
|
borderColor: 'rgba(58, 63, 153, 1)', // secondary-light
|
||||||
|
};
|
||||||
|
|
||||||
|
rootRef = React.createRef();
|
||||||
|
|
||||||
|
getGlobalStyle() {
|
||||||
|
if (!this.props.parentEl) {
|
||||||
|
return { display: 'none' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
position: 'absolute',
|
||||||
|
//padding: '5px',
|
||||||
|
background: 'bg-primary-dark',
|
||||||
|
//boxShadow: '0 0 4px rgba(0,0,0,.3)',
|
||||||
|
borderRadius: '3px',
|
||||||
|
//opacity: this.state.hover || this.props.active ? 1 : 0,
|
||||||
|
visibility: this.state.hover || this.props.active ? 'visible' : 'hidden',
|
||||||
|
zIndex: 50,
|
||||||
|
...this.getStyle(this.props.position, this.props.arrow),
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.mergeStyle(style, this.props.style.style);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBaseArrowStyle() {
|
||||||
|
return {
|
||||||
|
position: 'absolute',
|
||||||
|
content: '""',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getArrowStyle() {
|
||||||
|
let fgStyle = this.getBaseArrowStyle();
|
||||||
|
let bgStyle = this.getBaseArrowStyle();
|
||||||
|
fgStyle.zIndex = 60;
|
||||||
|
bgStyle.zIndex = 55;
|
||||||
|
|
||||||
|
let arrowStyle = {
|
||||||
|
...this.defaultArrowStyle,
|
||||||
|
...this.props.style.arrowStyle,
|
||||||
|
};
|
||||||
|
let bgBorderColor = arrowStyle.borderColor
|
||||||
|
? arrowStyle.borderColor
|
||||||
|
: 'transparent';
|
||||||
|
|
||||||
|
let fgColorBorder = `10px solid ${arrowStyle.color}`;
|
||||||
|
let fgTransBorder = `${FG_SIZE}px solid transparent`;
|
||||||
|
let bgColorBorder = `12px solid ${bgBorderColor}`;
|
||||||
|
let bgTransBorder = `${BG_SIZE}px solid transparent`;
|
||||||
|
|
||||||
|
let { position, arrow } = this.props;
|
||||||
|
|
||||||
|
if (position === 'left' || position === 'right') {
|
||||||
|
fgStyle.top = '50%';
|
||||||
|
fgStyle.borderTop = fgTransBorder;
|
||||||
|
fgStyle.borderBottom = fgTransBorder;
|
||||||
|
fgStyle.marginTop = -7;
|
||||||
|
|
||||||
|
bgStyle.borderTop = bgTransBorder;
|
||||||
|
bgStyle.borderBottom = bgTransBorder;
|
||||||
|
bgStyle.top = '50%';
|
||||||
|
bgStyle.marginTop = -8;
|
||||||
|
|
||||||
|
if (position === 'left') {
|
||||||
|
fgStyle.right = -10;
|
||||||
|
fgStyle.borderLeft = fgColorBorder;
|
||||||
|
bgStyle.right = -11;
|
||||||
|
bgStyle.borderLeft = bgColorBorder;
|
||||||
|
} else {
|
||||||
|
fgStyle.left = -9;
|
||||||
|
fgStyle.borderRight = fgColorBorder;
|
||||||
|
bgStyle.left = -11;
|
||||||
|
bgStyle.borderRight = bgColorBorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arrow === 'top') {
|
||||||
|
fgStyle.top = this.margin;
|
||||||
|
bgStyle.top = this.margin;
|
||||||
|
}
|
||||||
|
if (arrow === 'bottom') {
|
||||||
|
fgStyle.top = null;
|
||||||
|
fgStyle.bottom = this.margin - 7;
|
||||||
|
bgStyle.top = null;
|
||||||
|
bgStyle.bottom = this.margin - 8;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fgStyle.left = Math.round(this.state.width / 2 - FG_SIZE);
|
||||||
|
fgStyle.borderLeft = fgTransBorder;
|
||||||
|
fgStyle.borderRight = fgTransBorder;
|
||||||
|
fgStyle.marginLeft = 0;
|
||||||
|
bgStyle.left = fgStyle.left - 1;
|
||||||
|
bgStyle.borderLeft = bgTransBorder;
|
||||||
|
bgStyle.borderRight = bgTransBorder;
|
||||||
|
bgStyle.marginLeft = 0;
|
||||||
|
|
||||||
|
if (position === 'top') {
|
||||||
|
fgStyle.bottom = -10;
|
||||||
|
fgStyle.borderTop = fgColorBorder;
|
||||||
|
bgStyle.bottom = -11;
|
||||||
|
bgStyle.borderTop = bgColorBorder;
|
||||||
|
} else {
|
||||||
|
fgStyle.top = -10;
|
||||||
|
fgStyle.borderBottom = fgColorBorder;
|
||||||
|
bgStyle.top = -11;
|
||||||
|
bgStyle.borderBottom = bgColorBorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arrow === 'right') {
|
||||||
|
fgStyle.left = null;
|
||||||
|
fgStyle.right = this.margin + 1 - FG_SIZE;
|
||||||
|
bgStyle.left = null;
|
||||||
|
bgStyle.right = this.margin - FG_SIZE;
|
||||||
|
}
|
||||||
|
if (arrow === 'left') {
|
||||||
|
fgStyle.left = this.margin + 1 - FG_SIZE;
|
||||||
|
bgStyle.left = this.margin - FG_SIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
color,
|
||||||
|
borderColor,
|
||||||
|
...propsArrowStyle
|
||||||
|
} = this.props.style.arrowStyle;
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
fgStyle: this.mergeStyle(fgStyle, propsArrowStyle),
|
||||||
|
bgStyle: this.mergeStyle(bgStyle, propsArrowStyle),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.offscreenDifference > 0) {
|
||||||
|
if (state.fgStyle.top >= 0 || state.fgStyle.top < 0) {
|
||||||
|
state.fgStyle.top += this.offscreenDifference;
|
||||||
|
}
|
||||||
|
if (state.bgStyle.top >= 0 || state.bgStyle.top < 0) {
|
||||||
|
state.bgStyle.top += this.offscreenDifference;
|
||||||
|
}
|
||||||
|
if (typeof state.fgStyle.top === 'string') {
|
||||||
|
state.fgStyle.top = `calc(${state.fgStyle.top} + ${this.offscreenDifference}px)`;
|
||||||
|
}
|
||||||
|
if (typeof state.bgStyle.top === 'string') {
|
||||||
|
state.bgStyle.top = `calc(${state.bgStyle.top} + ${this.offscreenDifference}px)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeStyle(style, theme) {
|
||||||
|
if (theme) {
|
||||||
|
let {
|
||||||
|
position,
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
marginLeft,
|
||||||
|
marginRight,
|
||||||
|
...validTheme
|
||||||
|
} = theme;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...style,
|
||||||
|
...validTheme,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStyle(position, arrow) {
|
||||||
|
let alignOffset = 0;
|
||||||
|
let parent = this.props.parentEl;
|
||||||
|
let align = this.props.align;
|
||||||
|
let tooltipPosition = parent.getBoundingClientRect();
|
||||||
|
let scrollY =
|
||||||
|
window.scrollY !== undefined ? window.scrollY : window.pageYOffset;
|
||||||
|
let scrollX =
|
||||||
|
window.scrollX !== undefined ? window.scrollX : window.pageXOffset;
|
||||||
|
let top = scrollY + tooltipPosition.top;
|
||||||
|
let left = scrollX + tooltipPosition.left;
|
||||||
|
let style = {};
|
||||||
|
|
||||||
|
if (this.rootRef.current) {
|
||||||
|
const newHeight = this.rootRef.current.offsetHeight / 2;
|
||||||
|
const bottomPosition = tooltipPosition.bottom + newHeight;
|
||||||
|
const isOffscreen =
|
||||||
|
tooltipPosition.bottom + newHeight > window.innerHeight;
|
||||||
|
const offscreenDifference = bottomPosition - window.innerHeight;
|
||||||
|
if (isOffscreen) {
|
||||||
|
const padding = 3;
|
||||||
|
top -= offscreenDifference;
|
||||||
|
this.offscreenDifference = Math.min(
|
||||||
|
Math.max(offscreenDifference, 0),
|
||||||
|
newHeight - parent.getBoundingClientRect().height / 2 - padding
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.offscreenDifference = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentSize = {
|
||||||
|
width: parent.offsetWidth,
|
||||||
|
height: parent.offsetHeight,
|
||||||
|
};
|
||||||
|
|
||||||
|
// fix for svg
|
||||||
|
if (!parent.offsetHeight && parent.getBoundingClientRect) {
|
||||||
|
parentSize.width = parent.getBoundingClientRect().width;
|
||||||
|
parentSize.height = parent.getBoundingClientRect().height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (align === 'left') {
|
||||||
|
alignOffset = -parentSize.width / 2 + FG_SIZE;
|
||||||
|
} else if (align === 'right') {
|
||||||
|
alignOffset = parentSize.width / 2 - FG_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stylesFromPosition = {
|
||||||
|
left: () => {
|
||||||
|
style.top = top + parentSize.height / 2 - this.state.height / 2;
|
||||||
|
style.left = left - this.state.width - this.margin;
|
||||||
|
},
|
||||||
|
right: () => {
|
||||||
|
style.top = top + parentSize.height / 2 - this.state.height / 2;
|
||||||
|
style.left = left + parentSize.width + this.margin;
|
||||||
|
},
|
||||||
|
top: () => {
|
||||||
|
style.left =
|
||||||
|
left - this.state.width / 2 + parentSize.width / 2 + alignOffset;
|
||||||
|
style.top = top - this.state.height - this.margin;
|
||||||
|
},
|
||||||
|
bottom: () => {
|
||||||
|
style.left =
|
||||||
|
left - this.state.width / 2 + parentSize.width / 2 + alignOffset;
|
||||||
|
style.top = top + parentSize.height + this.margin;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const stylesFromArrow = {
|
||||||
|
left: () => {
|
||||||
|
style.left = left + parentSize.width / 2 - this.margin + alignOffset;
|
||||||
|
},
|
||||||
|
right: () => {
|
||||||
|
style.left =
|
||||||
|
left -
|
||||||
|
this.state.width +
|
||||||
|
parentSize.width / 2 +
|
||||||
|
this.margin +
|
||||||
|
alignOffset;
|
||||||
|
},
|
||||||
|
top: () => {
|
||||||
|
style.top = top + parentSize.height / 2 - this.margin;
|
||||||
|
},
|
||||||
|
bottom: () => {
|
||||||
|
style.top =
|
||||||
|
top + parentSize.height / 2 - this.state.height + this.margin;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
executeFunctionIfExist(stylesFromPosition, position);
|
||||||
|
executeFunctionIfExist(stylesFromArrow, arrow);
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkWindowPosition(style, arrowStyle) {
|
||||||
|
if (this.props.position === 'top' || this.props.position === 'bottom') {
|
||||||
|
if (style.left < 0) {
|
||||||
|
const parent = this.props.parentEl;
|
||||||
|
if (parent) {
|
||||||
|
const tooltipWidth = this.state.width;
|
||||||
|
let bgStyleRight = arrowStyle.bgStyle.right;
|
||||||
|
// For arrow = center
|
||||||
|
if (!bgStyleRight) {
|
||||||
|
bgStyleRight = tooltipWidth / 2 - BG_SIZE;
|
||||||
|
}
|
||||||
|
const newBgRight = Math.round(
|
||||||
|
bgStyleRight - style.left + this.margin
|
||||||
|
);
|
||||||
|
arrowStyle = {
|
||||||
|
...arrowStyle,
|
||||||
|
bgStyle: {
|
||||||
|
...arrowStyle.bgStyle,
|
||||||
|
right: newBgRight,
|
||||||
|
left: null,
|
||||||
|
},
|
||||||
|
fgStyle: {
|
||||||
|
...arrowStyle.fgStyle,
|
||||||
|
right: newBgRight + 1,
|
||||||
|
left: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
style.left = this.margin;
|
||||||
|
} else {
|
||||||
|
let rightOffset = style.left + this.state.width - window.innerWidth;
|
||||||
|
if (rightOffset > 0) {
|
||||||
|
let originalLeft = style.left;
|
||||||
|
style.left = window.innerWidth - this.state.width - this.margin;
|
||||||
|
arrowStyle.fgStyle.marginLeft += originalLeft - style.left;
|
||||||
|
arrowStyle.bgStyle.marginLeft += originalLeft - style.left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { style, arrowStyle };
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseEnter = () => {
|
||||||
|
this.props.active && this.props.useHover && this.setState({ hover: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleMouseLeave = () => {
|
||||||
|
this.setState({ hover: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.updateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (this.props !== prevProps) {
|
||||||
|
this.updateSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSize() {
|
||||||
|
const newWidth = this.rootRef.current.offsetWidth;
|
||||||
|
const newHeight = this.rootRef.current.offsetHeight;
|
||||||
|
|
||||||
|
if (newWidth !== this.state.width || newHeight !== this.state.height) {
|
||||||
|
this.setState({
|
||||||
|
width: newWidth,
|
||||||
|
height: newHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { style, arrowStyle } = this.checkWindowPosition(
|
||||||
|
this.getGlobalStyle(),
|
||||||
|
this.getArrowStyle()
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
onMouseEnter={this.handleMouseEnter}
|
||||||
|
onMouseLeave={this.handleMouseLeave}
|
||||||
|
ref={this.rootRef}
|
||||||
|
>
|
||||||
|
{this.props.arrow ? (
|
||||||
|
<div>
|
||||||
|
<span style={arrowStyle.fgStyle} />
|
||||||
|
<span style={arrowStyle.bgStyle} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const executeFunctionIfExist = (object, key) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(object, key)) {
|
||||||
|
object[key]();
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in new issue
Block a user