feat: Add DICOM SEG support and MPR, referenceLines and StackSync and more (#3015)

* feat: add initial sop class handler for SEG

* feat: move segmentation service

* update segmentation service methods

* fix: viewport data structure

* feat: Add initial render for the DICOM SEG for each displaySet

* fix: various wrong architectural dependencies between services

* feat: initial separate SEG display in each viewport

* feat: refactore viewport action bar

* initial work for SEG hydration

* fix: various bugs regarding drag and dropping different viewports

* fix: rendering issues for multiple seg displaysets

* fix: bugs for thumbnail and hydration

* feat: fix the initial segment color

* update after rebase

* feat: initial design for the segmentation group table

* feat: initial new design for the segmentation panel

* feat: segmentation panel

* feat: make segmentation appear on all related viewports

* fix: segmentation load bug based on functional groups

* initial work for segmentation crosshairs

* fix: various stylings and functionality

* fix: various stylings for the seg panel

* fix: overflow styles

* feat: add more ui components

* feat: added segmentation config

* feat: add jump to segment

* feat: add jump to segment for DICOM SEG viewport

* fix: bugs after rebase

* feat: jump in segmentation viewport

* fix: mpr support for seg

* feat: add more icons

* feat: new icons

* feat: add new side panel

* feat: add segmentation config

* feat: add loading indicator to ohif

* feat: make hanging protocols follow matching rules for viewports

* feat: enhance drag and drop to be hanging protocol aware

* fix: mpr restore previous layout

* fix: crosshairs toggle

* fix: add auth headers to the dicom loader via dicomwebclient

* fix: bug for crosshairs toggle in mpr

* fix: seg viewport reusing old toolGroup

* feat: add loading animation with lottie

* fix: various bugs for Segmentation hydration in mpr

* feat: change outline alpha to outline opacity

* feat: loading indicator for seg viewport

* fix: various segmentation group styles

* fix: local mode for seg

* feat: add animation for the highlight

* fix: loading indicatro to show segment indices

* feat: enhance modality drop down ui

* fix: panels

* fix: download form

* fix: layout shift in loading indicator

* fix: loading indicator to have correct values

* fix: update software number

* fix: image jump between MPR and default

* fix: segmentation cleanup and cine service cleanup

* fix: segmentation toolgroup clena up

* fix: highlight interval should not trigger again

* fix: issue with multiframe sorting

* rename: change onSeriesChange to onArrowsClick

* fix: buttons for tmtv and layout shift

* fix: various bugs wrt crosshairs

* fix: crosshairs re init on reset camera

* fix: middle slice calculation different from cs middle reset camera

* fix: reset camera should reset viewport camera

* fix: loading segments in MPR mode

* fix: tmtv hp back to before

* fix: layout shift

* feat: orientation markers for volume viewport

* fix: capture for volume viewports

* fix: memory leak for back to worklist

* fix: loading bar bg color

* fix: side panel for only one panel

* fix: react select style in production

* fix: various styling for segmentation groups

* fix: various ui styles

* feat: add new segmentation config styles

* fix: hover state for segmentation item

* fix: side panel layout shift

* temp add panels

* try to fix scrollbar for thimbnail

* fix: scroll not appearing for side panels

* feat: changed styles for scrollbar

* feat: add cpu fallback warning

* fix: webworker destroy

* fix: select styles

* fix: orientation marker color and position

* fix: capture screenshot for volume viewports

* fix: hide segment visibility on mpr

* fix: reloading an already loaded seg displayset

* feat: add is equal

* fix: mpr jump to measurements

* apply review comments

* fix: optimization for the segmentation load

* fix: remove unnecessary context menu and hp service reset

* fix: segmentation service wrt brush settings

* feat: initial work for the stack synchronization

* fix: add validateDisplaySetSelectorsForNewDisplaySets to make sure drag and drop can follow requirements

* fix: various react proptypes

* fix: thumbnails for the tmtv and change default props to default params

* feat: make showing loading indicator configurable and add docs

* bump versions and add more docs

* fix demo

* update cornerstone versions

* apply review comments

* feat: add reference lines

* fix build

* fix unit tests

* add reference lines icon

* fix: e2e tests

* fix static wado config

* docs: add segmentation service docs

* fix: docker build

* fix: keep camera and bump versions

* Try re-enabling minification to fix deploy previews

Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>
This commit is contained in:
AlirezaandErik Ziegler authored and GitHub committed 2022-11-16 14:19:52 -05:00
1 parent 08c58b0634
commit 83ea228d86
254 files changed
+12856 -3141

No files matched your search

+1
View File
@@ -45,6 +45,7 @@
"moment": "^2.24.0",
"query-string": "^6.14.0",
"object-hash": "2.1.1",
"gl-matrix": "^3.4.3",
"validate.js": "^0.12.0"
},
"devDependencies": {
-1
View File
@@ -33,7 +33,6 @@ describe('Top level exports', () => {
'ToolBarService',
'Types',
'ViewportGridService',
'SegmentationService',
'HangingProtocolService',
'UserAuthenticationService',
'IWebApiDataSource',
-3
View File
@@ -27,7 +27,6 @@ import {
HangingProtocolService,
pubSubServiceInterface,
UserAuthenticationService,
SegmentationService,
} from './services';
import IWebApiDataSource from './DataSources/IWebApiDataSource';
@@ -68,7 +67,6 @@ const OHIF = {
ViewportGridService,
HangingProtocolService,
UserAuthenticationService,
SegmentationService,
IWebApiDataSource,
DicomMetadataStore,
pubSubServiceInterface,
@@ -104,7 +102,6 @@ export {
ViewportGridService,
HangingProtocolService,
UserAuthenticationService,
SegmentationService,
IWebApiDataSource,
DicomMetadataStore,
pubSubServiceInterface,
@@ -124,9 +124,9 @@ describe('HangingProtocolService', () => {
it('matches best image match', () => {
hps.run({ studies: [studyMatch], displaySets: studyMatchDisplaySets });
const { hpAlreadyApplied, viewportMatchDetails } = hps.getMatchDetails();
expect(hpAlreadyApplied).toMatchObject([false]);
expect(viewportMatchDetails.length).toBe(1);
expect(viewportMatchDetails[0]).toMatchObject({
expect(hpAlreadyApplied).toMatchObject(new Map([[0, false]]));
expect(viewportMatchDetails.size).toBe(1);
expect(viewportMatchDetails.get(0)).toMatchObject({
viewportOptions: {
viewportId: 'ctAXIAL',
viewportType: 'volume',
@@ -1,3 +1,4 @@
import uuidv4 from '../../utils/uuidv4';
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
import sortBy from '../../utils/sortBy';
import ProtocolEngine from './ProtocolEngine';
@@ -11,9 +12,11 @@ const EVENTS = {
NEW_LAYOUT: 'event::hanging_protocol_new_layout',
CUSTOM_IMAGE_LOAD_PERFORMED:
'event::hanging_protocol_custom_image_load_performed',
HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT:
'event::hanging_protocol_applied_for_viewport',
};
type Protocol = HangingProtocol.Protocol | HangingProtocol.ProtocolGenerator;
type Protocol = HangingProtocol.Protocol;
class HangingProtocolService {
studies: StudyMetadata[];
@@ -27,7 +30,6 @@ class HangingProtocolService {
_commandsManager: Record<string, unknown>;
_servicesManager: Record<string, unknown>;
protocolEngine: ProtocolEngine;
hpAlreadyApplied: boolean[] = [];
customViewportSettings = [];
displaySets: IDisplaySet[] = [];
activeStudy: Record<string, unknown>;
@@ -54,19 +56,30 @@ class HangingProtocolService {
return prev;
}, []),
},
isReconstructable: {
name: 'Checks if the display set is reconstructable',
// we can add more advanced checking here
callback: displaySet => displaySet.isReconstructable ?? false,
},
};
listeners = {};
registeredImageLoadStrategies = {};
activeImageLoadStrategyName = null;
customImageLoadPerformed = false;
/**
* Whether the hanging protocol is applied for the given viewport index,
* applying means that the displayset(s) is set for the viewport to be shown
*/
hpAlreadyApplied: Map<number, boolean> = new Map();
/**
* displaySetMatchDetails = <displaySetId, match>
* DisplaySetId is the id defined in the hangingProtocol object itself
* and match is an object that contains information about
*/
displaySetMatchDetails: Map<
string,
string, // protocol displaySetId in the displayset selector
HangingProtocol.DisplaySetMatchDetails
> = new Map();
@@ -74,7 +87,10 @@ class HangingProtocolService {
* An array that contains for each viewport (viewportIndex) specified in the
* hanging protocol, an object of the form
*/
viewportMatchDetails = [] as HangingProtocol.ViewportMatchDetails[];
viewportMatchDetails: Map<
number, // viewportIndex
HangingProtocol.ViewportMatchDetails
> = new Map();
constructor(commandsManager, servicesManager) {
this._commandsManager = commandsManager;
@@ -94,12 +110,16 @@ class HangingProtocolService {
Object.assign(this, pubSubServiceInterface);
}
public destroy() {
this.reset();
this.protocols = new Map();
}
public reset() {
this.studies = [];
this.protocols = new Map();
this.hpAlreadyApplied = [];
this.viewportMatchDetails = [];
// this.ProtocolEngine.reset()
this.hpAlreadyApplied = new Map();
this.viewportMatchDetails = new Map();
this.displaySetMatchDetails = new Map();
}
public getDefaultProtocol(): HangingProtocol.Protocol {
@@ -148,21 +168,7 @@ class HangingProtocolService {
public getProtocolById(id: string): HangingProtocol.Protocol {
const protocol = this.protocols.get(id);
if (protocol instanceof Function) {
try {
const { protocol: generatedProtocol } = this._getProtocolFromGenerator(
protocol
);
return generatedProtocol;
} catch (error) {
console.warn(
`Error while executing protocol generator for protocol ${id}: ${error}`
);
}
} else {
return protocol;
}
return protocol;
}
/**
@@ -207,9 +213,7 @@ class HangingProtocolService {
* Sets the active hanging protocols to use, by name. If the value is empty,
* then resets the active protocols to all the named items.
*/
public setActiveProtocols(
hangingProtocol?: string[] | string
): void {
public setActiveProtocols(hangingProtocol?: string[] | string): void {
if (!hangingProtocol || !hangingProtocol.length) {
this.activeProtocolIds = null;
console.log('No active protocols, setting all to active');
@@ -289,8 +293,21 @@ class HangingProtocolService {
}
}
public setHangingProtocolAppliedForViewport(i): void {
this.hpAlreadyApplied[i] = true;
setHangingProtocolAppliedForViewport(i, status, suppressEvent = false) {
this.hpAlreadyApplied.set(i, status);
const numberOfViewports = this.viewportMatchDetails.size;
const numberOfViewportsApplied = Array.from(
this.hpAlreadyApplied.values()
).filter(applied => applied).length;
const progress = Math.round(
(numberOfViewportsApplied / numberOfViewports) * 100
);
this._broadcastChange(this.EVENTS.HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT, {
progress: progress,
});
}
/**
@@ -405,55 +422,181 @@ class HangingProtocolService {
return protocol;
}
getViewportsRequireUpdate(viewportIndex, displaySetInstanceUID) {
const newDisplaySetInstanceUID = displaySetInstanceUID;
const protocol = this.protocol;
const protocolStage = protocol.stages[this.stage];
const protocolViewports = protocolStage.viewports;
const protocolViewport = protocolViewports[viewportIndex];
const defaultReturn = [
{
viewportIndex,
displaySetInstanceUIDs: [newDisplaySetInstanceUID],
},
];
// if no viewport, then we can assume there is no predefined set of
// rules that should be applied to this viewport while matching
if (!protocolViewport) {
return defaultReturn;
}
// no support for drag and drop into fusion viewports yet
// Todo: smart drag and drop would look at the displaySets and
// replace the same modality type, but later
if (protocolViewport.displaySets.length > 1) {
throw new Error('Cannot update viewport with multiple displaySets yet');
}
// If there is no displaySet, then we can assume that the viewport
// is empty and we can just add the new displaySet to it
if (protocolViewport.displaySets.length === 0) {
return defaultReturn;
}
// if the viewport is not empty, then we check the displaySets it is showing
// currently, which means we need to check if the requested updated displaySet
// follow the same rules as the current displaySets
const displaySetSelectorId = protocolViewport.displaySets[0].id;
const displaySetSelector =
protocol.displaySetSelectors[displaySetSelectorId];
if (!displaySetSelector) {
return defaultReturn;
}
// so let's check if the new displaySetInstanceUIDs follow the same rules
this._validateViewportSpecificMatch(
{
displaySetInstanceUIDs: [newDisplaySetInstanceUID],
viewportOptions: {},
displaySetOptions: [],
},
protocolViewport,
protocol.displaySetSelectors
);
// if we reach here, it means there are some rules that should be applied
// if we don't have any match details for the displaySetSelector the viewport
// is currently showing, then we can assume that the new displaySetInstanceUID
// does not
if (!this.displaySetMatchDetails.get(displaySetSelectorId)) {
return defaultReturn;
}
// if we reach here, it means that the displaySetInstanceUIDs to be dropped
// in the viewportIndex are valid, and we can proceed with the update. However
// we need to check if the displaySets that the viewport were showing
// was also referenced by other viewports, and if so, we need to update those
// viewports as well
// check if displaySetSelectors are used by other viewports, and
// store the viewportIndex and displaySetInstanceUIDs that need to be updated
const viewportsToUpdate = [];
protocolViewports.forEach((viewport, index) => {
let viewportNeedsUpdate;
for (const displaySet of viewport.displaySets) {
if (displaySet.id === displaySetSelectorId) {
viewportNeedsUpdate = true;
break;
}
}
if (viewportNeedsUpdate) {
// we can then loop over the displaySets and choose all of them,
// but for the one that matches the oldDisplaySetInstanceUID we need to
// replace it with the newDisplaySetInstanceUID
const {
displaySetInstanceUIDs,
displaySetOptions,
} = viewport.displaySets.reduce(
(acc, displaySet) => {
const { id, options } = displaySet;
let {
displaySetInstanceUID: displaySetInstanceUIDToUse,
} = this.displaySetMatchDetails.get(id);
if (displaySet.id === displaySetSelectorId) {
displaySetInstanceUIDToUse = newDisplaySetInstanceUID;
}
acc.displaySetInstanceUIDs.push(displaySetInstanceUIDToUse);
acc.displaySetOptions.push(options);
return acc;
},
{ displaySetInstanceUIDs: [], displaySetOptions: [] }
);
viewportsToUpdate.push({
viewportIndex: index,
displaySetInstanceUIDs,
viewportOptions: viewport.viewportOptions,
displaySetOptions,
});
}
});
return viewportsToUpdate;
}
/**
* It applied the protocol to the current studies and display sets based on the
* protocolId that is provided.
* @param protocolId - name of the protocol to be set
* @param protocol - protocol object (optional), if not provided, the protocol
* will be retrieved from the list of protocols by its name
* @param matchingDisplaySets - predefined display sets to be used for the protocol
* @param protocolId - name of the registered protocol to be set
* @param options - options to be passed to the protocol, this is either an array
* of the displaySetInstanceUIDs to be set on ALL VIEWPORTS OF THE PROTOCOL or an object
* that contains viewportIndex as the key and displaySetInstanceUIDs as the value
* for each viewport that needs to be set.
* @param errorCallback - callback to be called if there is an error
* during the protocol application
*
* @returns boolean - true if the protocol was applied and no errors were found
*/
public setProtocol(
protocolId: string,
protocol?: HangingProtocol.Protocol,
matchingDisplaySets?: Record<string, HangingProtocol.DisplaySetMatchDetails>
options = {} as HangingProtocol.SetProtocolOptions,
errorCallback = null
): void {
if (!protocol) {
const foundProtocol = this.protocols.get(protocolId);
const foundProtocol = this.protocols.get(protocolId);
if (!foundProtocol) {
console.warn(
`HangingProtocolService::setProtocol - protocol ${protocolId} not found`
);
return;
}
if (foundProtocol instanceof Function) {
try {
({ protocol, matchingDisplaySets } = this._getProtocolFromGenerator(
foundProtocol
));
} catch (error) {
console.warn(
`HangingProtocolService::setProtocol - protocol ${protocolId} failed to execute`,
error
);
return;
}
} else {
protocol = foundProtocol;
}
if (!foundProtocol) {
console.warn(
`ProtocolEngine::setProtocol - Protocol with id ${protocolId} not found - you should register it first via addProtocol`
);
return;
}
this._setProtocol(protocol, matchingDisplaySets);
const protocol = this._validateProtocol(foundProtocol);
if (options) {
this._validateOptions(options);
}
try {
this._setProtocol(protocol, options);
} catch (error) {
console.error(error);
if (errorCallback) {
errorCallback(error);
}
throw new Error(error);
}
}
private _setProtocol(
protocol: HangingProtocol.Protocol,
matchingDisplaySets?: Record<string, HangingProtocol.DisplaySetMatchDetails>
options = null as HangingProtocol.SetProtocolOptions
): void {
this.stage = 0;
this.protocol = protocol;
const oldProtocol = this.protocol;
this.protocol = this._copyProtocol(protocol);
const { imageLoadStrategy } = protocol;
if (imageLoadStrategy) {
// check if the imageLoadStrategy is a valid strategy
@@ -464,12 +607,19 @@ class HangingProtocolService {
this.activeImageLoadStrategyName = imageLoadStrategy;
}
}
this._updateViewports(matchingDisplaySets);
try {
this._updateViewports(options);
} catch (error) {
this.protocol = oldProtocol;
throw new Error(error);
}
this._broadcastChange(this.EVENTS.PROTOCOL_CHANGED, {
viewportMatchDetails: this.viewportMatchDetails,
displaySetMatchDetails: this.displaySetMatchDetails,
hpAlreadyApplied: this.hpAlreadyApplied,
protocol: this.protocol,
});
}
@@ -498,48 +648,42 @@ class HangingProtocolService {
return this.protocol.stages[this.stage];
}
private _getProtocolFromGenerator(
protocolGenerator: HangingProtocol.ProtocolGenerator
): {
protocol: HangingProtocol.Protocol;
matchingDisplaySets: Record<string, HangingProtocol.DisplaySetMatchDetails>;
} {
const { protocol, matchingDisplaySets } = protocolGenerator({
servicesManager: this._servicesManager,
commandsManager: this._commandsManager,
});
const validatedProtocol = this._validateProtocol(protocol);
return {
protocol: validatedProtocol,
matchingDisplaySets,
};
}
/**
* Updates the viewports with the selected protocol stage.
*/
_updateViewports(
matchingDisplaySets?: Record<string, HangingProtocol.DisplaySetMatchDetails>
): void {
_updateViewports(options = null as HangingProtocol.SetProtocolOptions): void {
// Make sure we have an active protocol with a non-empty array of display sets
if (!this._getNumProtocolStages()) {
console.log('No protocol stages - nothing to display');
return;
throw new Error('No protocol or stages found');
}
// each time we are updating the viewports, we need to reset the
// matching applied
this.hpAlreadyApplied = [];
// reset displaySetMatchDetails
// Todo: we can have more intelligent invalidation of the hpAlreadyApplied
// since sometimes we are just updating some viewports and the rest are
// already applied (e.g. when we are using the drag and drop)
this.hpAlreadyApplied = new Map();
this.viewportMatchDetails = new Map();
this.displaySetMatchDetails = new Map();
this.customImageLoadPerformed = false;
if (matchingDisplaySets) {
this.displaySetMatchDetails = new Map(
Object.entries(matchingDisplaySets)
);
if (options) {
// if the options are defined, we can fill in the displaySetMatchDetails
// but we need to also check that any displaySetInstanceUIDs that are
// provided either at viewport level or at the protocol level SATISFIES
// the required seriesMatching criteria species in the protocol. Otherwise
// we need to throw an error. This way protocols become more strict and
// for instance don't allow drag and drop of displaySets that don't match
// (in MPR protocol we specify in the displaysetSelector that the displaySet
// to be used should be reconstructable. Or you can specify that the displaySet
// should be a localizer only for a protocol)
// options can be either an object with { displaySetInstanceUIDs, viewportOptions, displaySetOptions }
// options (global options), or an object of objects with viewportIndex
// as the key and the { displaySetInstanceUIDs, viewportOptions, displaySetOptions } as the value
// The following function will update the displaySetMatchDetails in place
this._updateMatchByOptions(this.protocol, options);
}
const { displaySetSelectors = {} } = this.protocol;
@@ -559,9 +703,7 @@ class HangingProtocolService {
return;
}
this.customImageLoadPerformed = false;
const { layoutType } = stageModel.viewportStructure;
// Retrieve the properties associated with the current display set's viewport structure template
// If no such layout properties exist, stop here.
const layoutProps = stageModel.viewportStructure.properties;
@@ -580,10 +722,21 @@ class HangingProtocolService {
});
// Matching the displaySets
for (const viewport of stageModel.viewports) {
for (
let viewportIndex = 0;
viewportIndex < numCols * numRows;
viewportIndex++
) {
if (viewportIndex >= stageModel.viewports.length) {
// If we have more viewports than display sets, stop here.
break;
}
const viewport = stageModel.viewports[viewportIndex];
for (const displaySet of viewport.displaySets) {
const { id: displaySetId } = displaySet;
// skip matching if already matched
// skip matching if already matched (e.g. by options above)
if (this.displaySetMatchDetails.has(displaySetId)) {
continue;
}
@@ -606,9 +759,8 @@ class HangingProtocolService {
// Loop through each viewport
stageModel.viewports.forEach((viewport, viewportIndex) => {
const { viewportOptions } = viewport;
this.hpAlreadyApplied.push(false);
const { viewportOptions = {} } = viewport;
this.hpAlreadyApplied.set(viewportIndex, false);
// DisplaySets for the viewport, Note: this is not the actual displaySet,
// but it is a info to locate the displaySet from the displaySetService
const displaySetsInfo = [];
@@ -646,13 +798,328 @@ class HangingProtocolService {
}
);
this.viewportMatchDetails[viewportIndex] = {
this.viewportMatchDetails.set(viewportIndex, {
viewportOptions,
displaySetsInfo,
};
});
});
}
_updateMatchByOptions(
protocol: Protocol,
options: HangingProtocol.SetProtocolOptions
) {
const { DisplaySetService } = this._servicesManager.services;
if (options.displaySetInstanceUIDs) {
this._updateGlobalMatchByOptions(
options as HangingProtocol.GlobalProtocolOptions,
protocol,
DisplaySetService
);
return;
}
// Todo: I don't think we need the following anymore, since the drag and
// drop has been reworked to ask HangingProtocolService to check
// which viewports need to be updated. I don't think there is other use cases
// other than drag and drop and thumbnails double click that will specify
// options at viewport level. if there are the following code will need to be
// uncommented and tested.
this._updateViewportSpecificMatchByOptions(
options as HangingProtocol.ViewportSpecificProtocolOptions,
protocol
);
}
private _updateViewportSpecificMatchByOptions(
options: HangingProtocol.ViewportSpecificProtocolOptions,
protocol: HangingProtocol.Protocol
) {
const { DisplaySetService } = this._servicesManager.services;
const { displaySetSelectors = {} } = protocol;
const protocolViewports = protocol.stages[this.stage].viewports;
// if we get here, we can fill in the displaySetMatchDetails
for (const viewportIndex in options) {
const displaySetAndViewportOptions = options[viewportIndex];
const protocolViewport = protocolViewports[viewportIndex];
// if the protocol already has the viewport
if (protocolViewport) {
// if the protocol has a viewport with specific displaySets, we need to check if the
// displaySetInstanceUIDs are allowed by the protocol
this._validateViewportSpecificMatch(
displaySetAndViewportOptions,
protocolViewport,
displaySetSelectors
);
displaySetAndViewportOptions.displaySetInstanceUIDs.forEach(
(displaySetInstanceUID, index) => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const displaySetId = protocolViewport.displaySets[index].id;
// since we are setting the displaySetMatchDetails for each viewport
// directly, a side effect is that any other viewport that is referencing
// the same displaySetId will also be updated with the new
// displaySetMatchDetails, but sometimes this is not a desired behavior
// which is why we are using the syncDataForViewports to determine if
// we should update the displaySetMatchDetails for all the viewports
// that are affected by the syncDataForViewports
if (protocol.syncDataForViewports) {
this.displaySetMatchDetails.set(displaySetId, {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
matchDetails: {},
matchingScores: [],
sortingInfo: {},
});
} else {
// if the protocol does not have the syncDataForViewports, we need to
// update the displaySetMatchDetails by introducing the displaySetIndex
// to the displaySetMatchDetails. This way we can match the displaySetInstanceUIDs
// for the viewportIndex, but also for the other viewports that are affected
// by the syncDataForViewports
const displaySetSelectorId = `${displaySetId}_${uuidv4()}`;
// update the displaySetId at the viewport
protocolViewport.displaySets[index].id = displaySetSelectorId;
this.displaySetMatchDetails.set(displaySetSelectorId, {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
matchDetails: {},
matchingScores: [],
sortingInfo: {},
});
}
}
);
} else {
// if the protocol does not have the viewport, we need to create it
const newViewport = {
displaySets: [],
viewportOptions: {},
};
displaySetAndViewportOptions?.displaySetInstanceUIDs?.forEach(
(displaySetInstanceUID, index) => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const displaySetId = `viewport_${viewportIndex}_displaySet_${index}`;
newViewport.displaySets.push({
id: displaySetId,
});
this.displaySetMatchDetails.set(displaySetId, {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
matchDetails: {},
matchingScores: [],
sortingInfo: {},
});
}
);
protocolViewports[viewportIndex] = newViewport;
}
}
}
private _validateViewportSpecificMatch(
displaySetAndViewportOptions: HangingProtocol.DisplaySetAndViewportOptions,
protocolViewport: HangingProtocol.Viewport,
displaySetSelectors: Record<string, HangingProtocol.DisplaySetSelector>
) {
const { DisplaySetService } = this._servicesManager.services;
const protocolViewportDisplaySets = protocolViewport.displaySets;
const numDisplaySetsToSet =
displaySetAndViewportOptions.displaySetInstanceUIDs.length;
if (
protocolViewportDisplaySets.length > 0 &&
numDisplaySetsToSet !== protocolViewportDisplaySets.length
) {
throw new Error(
`The number of displaySets to set ${numDisplaySetsToSet} does not match the number of displaySets in the protocol ${protocolViewportDisplaySets} - not currently implemented`
);
}
displaySetAndViewportOptions.displaySetInstanceUIDs.forEach(
displaySetInstanceUID => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
const { displaySets: displaySetsInfo } = protocolViewport;
for (const displaySetInfo of displaySetsInfo) {
const displaySetSelector = displaySetSelectors[displaySetInfo.id];
if (!displaySetSelector) {
continue;
}
this._validateRequiredSelectors(displaySetSelector, displaySet);
}
}
);
}
private _updateGlobalMatchByOptions(
options: HangingProtocol.GlobalProtocolOptions,
protocol: Protocol,
DisplaySetService: any
) {
const { displaySetSelectors = {} } = protocol;
const protocolViewports = protocol.stages[this.stage].viewports;
options = options as HangingProtocol.GlobalProtocolOptions;
// we need to check each displaySetInstanceUIDs to see if it satisfies the
// seriesMatching criteria
options.displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
// match against all the displaySetSelectors defined in the protocol
for (const displaySetId in displaySetSelectors) {
const displaySetSelector = displaySetSelectors[displaySetId];
this._validateRequiredSelectors(displaySetSelector, displaySet);
}
});
// if we get here, we can fill in the displaySetMatchDetails
// however, there might be a case where we are asked to
// render multiple displaySets for a single viewport
// so we need to go back to viewports and start from there
const newDisplaySetIds: Set<string> = new Set();
// Todo: this currently work for current stage only
protocolViewports.forEach(viewport => {
viewport.displaySets.forEach(displaySetInfo => {
const { id: displaySetId } = displaySetInfo;
if (!this.displaySetMatchDetails.has(displaySetId)) {
newDisplaySetIds.add(displaySetId);
}
});
});
// Todo: handle override of the viewport and displaySet options
Array.from(newDisplaySetIds).forEach((displaySetId, index) => {
const displaySetInstanceUID = options.displaySetInstanceUIDs[index];
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
this.displaySetMatchDetails.set(displaySetId, {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
matchDetails: {},
matchingScores: [],
sortingInfo: {},
});
});
/*
Todo: make it work for the case where the number of displaySets
to set is not equal to the number of displaySets in the protocol, we can modify
the protocol to have the same number of displaySets for each viewport
if (newDisplaySetIds.size !== numberOfDisplaySetsToSet) {
// the remaining ones to set
const remainingDisplaySetMatches = options.slice(
newDisplaySetIds.size,
numberOfDisplaySetsToSet
);
protocol[this.stage].viewports.forEach(viewport => {
const { displaySets } = viewport;
// push the displaySetsToSet to the end of the displaySets array
remainingDisplaySetMatches.forEach(({ displaySetInstanceUID }) => {
displaySets.push({
id: displaySetInstanceUID,
});
});
});
remainingDisplaySetMatches.forEach(({ displaySetInstanceUID }) => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
this.displaySetMatchDetails.set(displaySetInstanceUID, {
SeriesInstanceUID: displaySet.SeriesInstanceUID,
StudyInstanceUID: displaySet.StudyInstanceUID,
displaySetInstanceUID: displaySet.displaySetInstanceUID,
matchDetails: {},
matchingScores: [],
sortingInfo: {},
});
});
}
// Todo: this has a bug where if the protocol defines only one displaySet,
// but renders a fusion, it cannot go back to one displaySet since its
// viewport.displaySets.length is 2 and it should remove the second one
// later
*/
}
private _validateRequiredSelectors(
displaySetSelector: HangingProtocol.DisplaySetSelector,
displaySet: any
) {
const { seriesMatchingRules } = displaySetSelector;
if (seriesMatchingRules.length) {
// only match the required rules
const requiredRules = seriesMatchingRules.filter(rule => rule.required);
const matched = this.protocolEngine.findMatch(displaySet, requiredRules);
if (!matched || matched.score === 0) {
throw new Error(
`The displaySetInstanceUID ${displaySet.displaySetInstanceUID} does not satisfy the required seriesMatching criteria for the protocol`
);
}
}
}
_validateOptions(options: HangingProtocol.SetProtocolOptions) {
const { DisplaySetService } = this._servicesManager.services;
if (options.displaySetInstanceUIDs) {
options = options as HangingProtocol.GlobalProtocolOptions;
options.displaySetInstanceUIDs.forEach(displaySetInstanceUID => {
const displaySet = DisplaySetService.getDisplaySetByUID(
displaySetInstanceUID
);
if (!displaySet) {
throw new Error(
`The displaySetInstanceUID ${displaySetInstanceUID} is not found in the displaySetService`
);
}
});
}
}
// Match images given a list of Studies and a Viewport's image matching reqs
_matchImages(displaySetRules) {
// TODO: matching is applied on study and series level, instance
@@ -821,12 +1288,12 @@ class HangingProtocolService {
* Changes the current stage to a new stage index in the display set sequence.
* It checks if the next stage exists.
*
* @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
* @param {Integer} stageAction An integer value specifying whether next (1) or previous (-1) stage
* @return {Boolean} True if new stage has set or false, otherwise
*/
_setCurrentProtocolStage(stageAction): boolean {
//reseting the applied protocols
this.hpAlreadyApplied = [];
//resetting the applied protocols
this.hpAlreadyApplied = new Map();
// Check if previous or next stage is available
if (stageAction === -1 && !this._isPreviousStageAvailable()) {
return false;
@@ -883,6 +1350,142 @@ class HangingProtocolService {
});
}
}
_copyProtocol(protocol: Protocol) {
return JSON.parse(JSON.stringify(protocol));
}
/**
_setProtocolLayoutOptions(protocol: Protocol, { numRows, numCols }) {
const layoutOptions = [];
const protocolStage = protocol.stages[this.stage];
const numViewports = protocolStage.viewports.length;
for (let i = 0; i < numViewports; i++) {
const { row, col } = unravelIndex(i, numRows, numCols);
const w = 1 / numCols;
const h = 1 / numRows;
const xPos = col * w;
const yPos = row * h;
layoutOptions[i] = {
width: w,
height: h,
x: xPos,
y: yPos,
};
}
// Todo: handle the case where the viewportStructure is not a grid
protocolStage.viewportStructure.properties.rows = numRows;
protocolStage.viewportStructure.properties.columns = numCols;
protocolStage.viewportStructure.properties.layoutOptions = {
...layoutOptions,
};
}
_getUpdatedProtocol({
numRows,
numCols,
protocol: oldProtocol,
}: {
numRows: number;
numCols: number;
protocol: Protocol;
}): Protocol {
let newProtocol = this._copyProtocol(oldProtocol);
const protocolStage = newProtocol.stages[this.stage];
// The following commented code is a potential improvements to the
// hanging protocols to intelligently switch between number of rows
// and columns based on the old state of the protocol. For instance,
// changing from 2x2 to 2x3 (adding a column) right now reorders the viewports
// as well, however, it should just add one empty column to the right
// and leave the rest of the viewports in place. This sounds amazing,
// but comes at a cost (which we need to tackle later). The cost is that
// the viewportIndex will change during this smart change of layout. In
// the example above the viewport at index (2) bottom left, will now be
// at index (3) bottom left, and since react will re-render the viewport
// it will reset the viewport's state such as (zoom, pan, windowLevel, imageIndex)
// and the user will lose their current state. In addition, all our viewportIds
// are dependent on the viewportIndex, so we will need to update all the viewportIds
// as well, and you can see how this can get out of hand. Later, we should
// tackle this problem and make the smart change of layout work.
// const { rows: oldNumRows, columns: oldNumCols } = newProtocol.stages[
// this.stage
// ].viewportStructure.properties;
// const oldToNewViewportIndices = getGridMapping(
// {
// numRows: oldNumRows,
// numCols: oldNumCols,
// },
// {
// numRows,
// numCols,
// }
// );
const protocolViewports = protocolStage.viewports;
if (protocolViewports.length < numRows * numCols) {
const newViewports = [];
for (let i = protocolViewports.length; i < numRows * numCols; i++) {
newViewports.push({
viewportOptions: {
toolGroupId: 'default',
viewportType: 'stack',
},
displaySets: [
{
id: `viewport-${i}`,
},
],
});
}
protocolStage.viewports = [...protocolViewports, ...newViewports];
} else if (protocolViewports.length > numRows * numCols) {
// remove viewports that are not needed
protocolStage.viewports = protocolViewports.slice(0, numRows * numCols);
}
// update the displaySetMatchDetails to reflect the new viewports
const toRemove = [];
this.displaySetMatchDetails.forEach(
(displaySetMatchDetail, displaySetId) => {
// if the displaySetId is not referenced in the protocolStage viewports
// we can remove it
const found = protocolStage.viewports.find(viewport => {
return viewport.displaySets.find(displaySet => {
return displaySet.id === displaySetId;
});
});
if (!found) {
toRemove.push(displaySetId);
}
}
);
toRemove.forEach(displaySetId => {
this.displaySetMatchDetails.delete(displaySetId);
});
this._setProtocolLayoutOptions(newProtocol, { numRows, numCols });
newProtocol = this._validateProtocol(newProtocol);
// Todo: not sure if we need to reset here, or we can smartly update the
// viewportMatchDetails and hpAlreadyApplied maps
this.hpAlreadyApplied = new Map();
this.viewportMatchDetails = new Map();
return newProtocol;
}
*/
}
export default HangingProtocolService;
@@ -0,0 +1,356 @@
// This can be calculated by some formula probably, but for now we just use a constant since
// this might be objective
const GRID_MAPPINGS = {
// 1x2
'1x2:1x1': {
0: 0,
},
'1x2:1x3': {
0: 0,
1: 1,
},
'1x2:2x1': {
0: 0,
1: 1,
},
'1x2:2x2': {
0: 0,
1: 1,
},
'1x2:2x3': {
0: 0,
1: 1,
},
'1x2:3x1': {
0: 0,
1: 1,
},
'1x2:3x2': {
0: 0,
1: 1,
},
'1x2:3x3': {
0: 0,
1: 1,
},
// 1x3
'1x3:1x1': {
0: 0,
},
'1x3:1x2': {
0: 0,
1: 1,
},
'1x3:2x1': {
0: 0,
1: 1,
},
'1x3:2x2': {
0: 0,
1: 1,
2: 2,
},
'1x3:2x3': {
0: 0,
1: 1,
2: 2,
},
'1x3:3x1': {
0: 0,
1: 1,
2: 2,
},
'1x3:3x2': {
0: 0,
1: 1,
2: 2,
},
'1x3:3x3': {
0: 0,
1: 1,
2: 2,
},
// 2x1
'2x1:1x2': {
0: 0,
1: 1,
},
'2x1:1x3': {
0: 0,
1: 1,
},
'2x1:2x2': {
0: 0,
2: 1,
},
'2x1:2x3': {
0: 0,
3: 1,
},
'2x1:3x1': {
0: 0,
1: 1,
},
'2x1:3x2': {
0: 0,
2: 1,
},
'2x1:3x3': {
0: 0,
3: 1,
},
// 2x2
'2x2:1x2': {
0: 0,
1: 1,
},
'2x2:1x3': {
0: 0,
1: 1,
2: 2,
},
'2x2:2x1': {
0: 0,
1: 2,
},
'2x2:2x3': {
0: 0,
1: 1,
3: 2,
4: 3,
},
'2x2:3x1': {
0: 0,
1: 1,
2: 2,
},
'2x2:3x2': {
0: 0,
1: 1,
2: 2,
3: 3,
},
'2x2:3x3': {
0: 0,
1: 1,
3: 2,
4: 3,
},
// 2x3
'2x3:1x2': {
0: 0,
1: 1,
},
'2x3:1x3': {
0: 0,
1: 1,
2: 2,
},
'2x3:2x1': {
0: 0,
1: 3,
},
'2x3:2x2': {
0: 0,
1: 1,
2: 3,
3: 4,
},
'2x3:3x1': {
0: 0,
1: 1,
2: 2,
},
'2x3:3x2': {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
},
'2x3:3x3': {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
},
// 3x1
'3x1:1x2': {
0: 0,
1: 1,
},
'3x1:1x3': {
0: 0,
1: 1,
2: 2,
},
'3x1:2x1': {
0: 0,
1: 1,
},
// TODO: I'm not sure about the following
'3x1:2x2': {
0: 0,
1: 1,
2: 2,
},
'3x1:2x3': {
0: 0,
1: 1,
2: 2,
},
'3x1:3x2': {
0: 0,
2: 1,
4: 2,
},
'3x1:3x3': {
0: 0,
3: 1,
6: 2,
},
// 3x2
'3x2:1x2': {
0: 0,
1: 1,
},
'3x2:1x3': {
0: 0,
1: 1,
2: 2,
},
'3x2:2x1': {
0: 0,
1: 2,
},
'3x2:2x2': {
0: 0,
1: 1,
2: 2,
3: 3,
},
'3x2:2x3': {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
},
'3x2:3x1': {
0: 0,
1: 2,
2: 4,
},
'3x2:3x3': {
0: 0,
1: 1,
3: 2,
4: 3,
6: 4,
7: 5,
},
// 3x3
'3x3:1x2': {
0: 0,
1: 1,
},
'3x3:1x3': {
0: 0,
1: 1,
2: 2,
},
'3x3:2x1': {
0: 0,
1: 3,
},
'3x3:2x2': {
0: 0,
1: 1,
2: 3,
3: 4,
},
'3x3:2x3': {
0: 0,
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
},
'3x3:3x1': {
0: 0,
1: 3,
2: 6,
},
'3x3:3x2': {
0: 0,
1: 1,
2: 3,
3: 4,
4: 6,
5: 7,
},
};
/**
* The purpose of this function is to convert a grid with numRows and numCols
* and index for each cell into another grid with different dimensions, but it should
* intelligently use the data from the original grid to fill the new grid at
* correct locations.
*
* For instance:
* if the old grid is a 2x2 (numRows = 2, numCols = 2) and the new grid is a 3x3
* it should intelligently insert the cells in the new grid so that the cells
* are added to the right most column. Then the mapping is as follows:
* 0 -> 0, 1 -> 1, 3 -> 2, 4 -> 3 (viewport 2 in the old grid can be used in
* the place of viewport 3 in the new grid)
*
* Or if the old grid is 2x2 and new grid is 2x4, the mapping is as follows:
* 0 -> 0, 1 -> 1, 4 -> 2 and 5 -> 3
*
* Or if the old grid is 2x2 and the new grid is 1x2, the mapping is as follows:
* 0 -> 0, 1 -> 2
*
* @param {Object} oldGrid
* @param {number} oldGrid.numRows
* @param {number} oldGrid.numCols
*
* @param {Object} newGrid
* @param {number} newGrid.numRows
* @param {number} newGrid.numCols
*
* @returns {Map} A map that maps the new indices to the old indices
*
*/
const getGridMapping = (oldGrid, newGrid) => {
const mapping = {};
const { numRows: oldNumRows, numCols: oldNumCols } = oldGrid;
const { numRows: newNumRows, numCols: newNumCols } = newGrid;
if (oldNumRows === 1 && oldNumCols === 1) {
// If the old grid is 1x1, then we can just return the first cell
mapping[0] = 0;
return mapping;
}
if (newNumRows === 1 && newNumCols === 1) {
// If the new grid is 1x1, then we can just return the first cell
mapping[0] = 0;
return mapping;
}
const key = `${oldNumRows}x${oldNumCols}:${newNumRows}x${newNumCols}`;
const map = GRID_MAPPINGS[key];
if (!map) {
throw new Error(`No mapping found for ${key}`);
}
return map;
};
export default getGridMapping;
@@ -1,136 +0,0 @@
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
const EVENTS = {
SEGMENTATION_UPDATED: 'event::segmentation_updated',
SEGMENTATION_ADDED: 'event::segmentation_added',
SEGMENTATION_REMOVED: 'event::segmentation_removed',
SEGMENTATION_VISIBILITY_CHANGED: 'event::SEGMENTATION_VISIBILITY_CHANGED',
};
const VALUE_TYPES = {};
class SegmentationService {
constructor() {
this.segmentations = {};
this.listeners = {};
Object.defineProperty(this, 'EVENTS', {
value: EVENTS,
writable: false,
enumerable: true,
configurable: false,
});
Object.assign(this, pubSubServiceInterface);
}
/**
* Get all segmentations.
*
* @return Array of segmentations
*/
getSegmentations() {
const segmentations = this._arrayOfObjects(this.segmentations);
return (
segmentations &&
segmentations.map(m => this.segmentations[Object.keys(m)[0]])
);
}
/**
* Get specific segmentation by its id.
*
* @param id If of the segmentation
* @return segmentation instance
*/
getSegmentation(id) {
return this.segmentations[id];
}
addOrUpdateSegmentation(
id,
segmentationSchema,
notYetUpdatedAtSource = false
) {
const segmentation = this.segmentations[id];
if (segmentation) {
Object.assign(segmentation, segmentationSchema);
this._broadcastEvent(this.EVENTS.SEGMENTATION_UPDATED, {
id,
segmentation,
notYetUpdatedAtSource: notYetUpdatedAtSource,
});
return;
}
this.segmentations[id] = {
...segmentationSchema,
visible: true,
};
this._broadcastEvent(this.EVENTS.SEGMENTATION_ADDED, {
id,
segmentation,
});
}
/**
* Toggles the visibility of a segmentation in the state, and broadcasts the event.
* Note: this method does not update the segmentation state in the source. It only
* updates the state, and there should be separate listeners for that.
* @param ids segmentation ids
*/
toggleSegmentationsVisibility(ids) {
ids.forEach(id => {
const segmentation = this.segmentations[id];
if (!segmentation) {
throw new Error(`Segmentation with id ${id} not found.`);
}
segmentation.visible = !segmentation.visible;
this._broadcastEvent(this.EVENTS.SEGMENTATION_VISIBILITY_CHANGED, {
segmentation,
});
});
}
/**
* Removes a segmentation and broadcasts the removed event.
*
* @param {string} id The segmentation id
*/
remove(id) {
if (!id || !this.segmentations[id]) {
console.warn(`No id provided, or unable to find segmentation by id.`);
return;
}
delete this.segmentations[id];
this._broadcastEvent(this.EVENTS.SEGMENTATION_REMOVED, {
id,
});
}
/**
* Clear all segmentations and broadcasts cleared event.
*/
clear() {
Object.keys(this.segmentations).forEach(id => {
this.remove(id);
});
this.segmentations = {};
}
/**
* Converts object of objects to array.
*
* @return {Array} Array of objects
*/
_arrayOfObjects = obj => {
return Object.entries(obj).map(e => ({ [e[0]]: e[1] }));
};
}
export default SegmentationService;
export { EVENTS, VALUE_TYPES };
@@ -1,8 +0,0 @@
import SegmentationService from './SegmentationService';
export default {
name: 'SegmentationService',
create: ({ configuration = {} }) => {
return new SegmentationService();
},
};
@@ -70,21 +70,34 @@ export default class ToolBarService {
break;
}
case 'tool': {
this.state.primaryToolId = itemId;
commands.forEach(
({ commandName = 'setToolActive', commandOptions, context }) => {
commandsManager.runCommand(commandName, commandOptions, context);
}
);
try {
commands.forEach(
({ commandName = 'setToolActive', commandOptions, context }) => {
commandsManager.runCommand(commandName, commandOptions, context);
}
);
// only set the primary tool if no error was thrown
this.state.primaryToolId = itemId;
} catch (error) {
console.warn(error);
}
break;
}
case 'toggle': {
const { commands } = interaction;
let commandExecuted;
// only toggle if a command was executed
this.state.toggles[itemId] =
this.state.toggles[itemId] === undefined
? true
: !this.state.toggles[itemId];
const { commands } = interaction;
if (!commands) {
break;
}
commands.forEach(({ commandName, commandOptions, context }) => {
if (!commandOptions) {
@@ -93,9 +106,21 @@ export default class ToolBarService {
if (commandName) {
commandOptions.toggledState = this.state.toggles[itemId];
commandsManager.runCommand(commandName, commandOptions, context);
try {
commandsManager.runCommand(commandName, commandOptions, context);
commandExecuted = true;
} catch (error) {
console.warn(error);
}
}
});
if (!commandExecuted) {
// If no command was executed, we need to toggle the state back
this.state.toggles[itemId] = !this.state.toggles[itemId];
}
break;
}
default:
@@ -1,95 +0,0 @@
const name = 'ViewportGridService';
const publicAPI = {
name,
getState: _getState,
setActiveViewportIndex: _setActiveViewportIndex,
setDisplaySetsForViewport: _setDisplaySetsForViewport,
setLayout: _setLayout,
setCachedLayout: _setCachedLayout,
setServiceImplementation,
reset: _reset,
set: _set,
};
const serviceImplementation = {
_getState: () => console.warn('getState() NOT IMPLEMENTED'),
_setActiveViewportIndex: () =>
console.warn('setActiveViewportIndex() NOT IMPLEMENTED'),
_setDisplaySetsForViewport: () =>
console.warn('setDisplaySetsForViewport() NOT IMPLEMENTED'),
_setLayout: () => console.warn('setLayout() NOT IMPLEMENTED'),
_reset: () => console.warn('reset() NOT IMPLEMENTED'),
_setCachedLayout: () => console.warn('setCachedLayout() NOT IMPLEMENTED'),
_set: () => console.warn('set() NOT IMPLEMENTED'),
};
function _getState() {
return serviceImplementation._getState();
}
function _setActiveViewportIndex(index) {
return serviceImplementation._setActiveViewportIndex(index);
}
function _setDisplaySetsForViewport({ viewportIndex, displaySetInstanceUIDs }) {
return serviceImplementation._setDisplaySetsForViewport({
viewportIndex,
displaySetInstanceUIDs,
});
}
function _setLayout({ numCols, numRows }) {
return serviceImplementation._setLayout({ numCols, numRows });
}
function _set(state) {
return serviceImplementation._set(state);
}
function _reset() {
return serviceImplementation._reset({});
}
function _setCachedLayout({ numCols, numRows, viewports }) {
return serviceImplementation._setLayout({ numCols, numRows, viewports });
}
function setServiceImplementation({
getState: getStateImplementation,
setActiveViewportIndex: setActiveViewportIndexImplementation,
setDisplaySetsForViewport: setDisplaySetsForViewportImplementation,
setCachedLayout: setCachedLayoutImplementation,
setLayout: setLayoutImplementation,
reset: resetImplementation,
set: setImplementation,
}) {
if (getStateImplementation) {
serviceImplementation._getState = getStateImplementation;
}
if (setActiveViewportIndexImplementation) {
serviceImplementation._setActiveViewportIndex = setActiveViewportIndexImplementation;
}
if (setDisplaySetsForViewportImplementation) {
serviceImplementation._setDisplaySetsForViewport = setDisplaySetsForViewportImplementation;
}
if (setLayoutImplementation) {
serviceImplementation._setLayout = setLayoutImplementation;
}
if (resetImplementation) {
serviceImplementation._reset = resetImplementation;
}
if (setCachedLayoutImplementation) {
serviceImplementation._setCachedLayout = setCachedLayoutImplementation;
}
if (setImplementation) {
serviceImplementation._set = setImplementation;
}
}
export default {
name,
create: ({ configuration = {} }) => {
return publicAPI;
},
};
@@ -0,0 +1,113 @@
import pubSubServiceInterface from './../_shared/pubSubServiceInterface';
const EVENTS = {
ACTIVE_VIEWPORT_INDEX_CHANGED: 'event::activeviewportindexchanged',
};
class ViewportGridService {
serviceImplementation = {};
EVENTS: { [key: string]: string };
listeners = {};
constructor() {
Object.assign(this, pubSubServiceInterface);
this.serviceImplementation = {};
this.EVENTS = EVENTS;
}
setServiceImplementation({
getState: getStateImplementation,
setActiveViewportIndex: setActiveViewportIndexImplementation,
setDisplaySetsForViewport: setDisplaySetsForViewportImplementation,
setDisplaySetsForViewports: setDisplaySetsForViewportsImplementation,
setCachedLayout: setCachedLayoutImplementation,
restoreCachedLayout: restoreCachedLayoutImplementation,
setLayout: setLayoutImplementation,
reset: resetImplementation,
set: setImplementation,
}) {
if (getStateImplementation) {
this.serviceImplementation._getState = getStateImplementation;
}
if (setActiveViewportIndexImplementation) {
this.serviceImplementation._setActiveViewportIndex = setActiveViewportIndexImplementation;
}
if (setDisplaySetsForViewportImplementation) {
this.serviceImplementation._setDisplaySetsForViewport = setDisplaySetsForViewportImplementation;
}
if (setDisplaySetsForViewportsImplementation) {
this.serviceImplementation._setDisplaySetsForViewports = setDisplaySetsForViewportsImplementation;
}
if (setLayoutImplementation) {
this.serviceImplementation._setLayout = setLayoutImplementation;
}
if (resetImplementation) {
this.serviceImplementation._reset = resetImplementation;
}
if (setCachedLayoutImplementation) {
this.serviceImplementation._setCachedLayout = setCachedLayoutImplementation;
}
if (restoreCachedLayoutImplementation) {
this.serviceImplementation._restoreCachedLayout = restoreCachedLayoutImplementation;
}
if (setImplementation) {
this.serviceImplementation._set = setImplementation;
}
}
public setActiveViewportIndex(index) {
this.serviceImplementation._setActiveViewportIndex(index);
this._broadcastEvent(this.EVENTS.ACTIVE_VIEWPORT_INDEX_CHANGED, {
viewportIndex: index,
});
}
public getState() {
return this.serviceImplementation._getState();
}
public setDisplaySetsForViewport({
viewportIndex,
displaySetInstanceUIDs,
viewportOptions,
displaySetOptions,
}) {
this.serviceImplementation._setDisplaySetsForViewport({
viewportIndex,
displaySetInstanceUIDs,
viewportOptions,
displaySetOptions,
});
}
public setDisplaySetsForViewports(viewports) {
this.serviceImplementation._setDisplaySetsForViewports(viewports);
}
public setLayout({ numCols, numRows }) {
this.serviceImplementation._setLayout({ numCols, numRows });
}
public reset() {
this.serviceImplementation._reset();
}
public setCachedLayout({ cacheId, cachedLayout }) {
this.serviceImplementation._setCachedLayout({ cacheId, cachedLayout });
}
public restoreCachedLayout(cacheId) {
this.serviceImplementation._restoreCachedLayout(cacheId);
}
public set(state) {
this.serviceImplementation._set(state);
}
}
export default {
name: 'ViewportGridService',
create: ({ configuration = {} }) => {
return new ViewportGridService();
},
};
-2
View File
@@ -12,7 +12,6 @@ import CineService from './CineService';
import HangingProtocolService from './HangingProtocolService';
import pubSubServiceInterface from './_shared/pubSubServiceInterface';
import UserAuthenticationService from './UserAuthenticationService';
import SegmentationService from './SegmentationService';
export {
MeasurementService,
@@ -29,5 +28,4 @@ export {
CineService,
pubSubServiceInterface,
UserAuthenticationService,
SegmentationService,
};
+27 -8
View File
@@ -18,10 +18,28 @@ type DisplaySetMatchDetails = {
sortingInfo?: any;
};
type DisplaySetAndViewportOptions = {
displaySetInstanceUIDs: string[];
viewportOptions: ViewportOptions;
displaySetOptions: DisplaySetOptions;
}
type ViewportSpecificProtocolOptions = {
[viewportIndex: string]: DisplaySetAndViewportOptions
}
type GlobalProtocolOptions = DisplaySetAndViewportOptions
type SetProtocolOptions =
ViewportSpecificProtocolOptions | GlobalProtocolOptions;
type HangingProtocolMatchDetails = {
displaySetMatchDetails: Map<string, DisplaySetMatchDetails>;
viewportMatchDetails: ViewportMatchDetails[];
hpAlreadyApplied: boolean[];
viewportMatchDetails: Map<number, ViewportMatchDetails>;
hpAlreadyApplied: Map<number, boolean>;
};
type MatchingRule = {
@@ -130,15 +148,12 @@ type Protocol = {
imageLoadStrategy?: string; // Todo: this should be types specifically
protocolMatchingRules?: MatchingRule[];
numberOfPriorsReferenced?: number;
syncDataForViewports?: boolean;
};
type ProtocolGenerator = ({servicesManager: any, commandsManager: any}) => {
protocol: Protocol;
matchingDisplaySets: any;
};
export type {
ProtocolGenerator,
SetProtocolOptions,
ViewportOptions,
ViewportMatchDetails,
DisplaySetMatchDetails,
@@ -153,5 +168,9 @@ export type {
MatchingRule,
SyncGroup,
initialImageOptions,
DisplaySetInfo
DisplaySetInfo,
GlobalProtocolOptions,
ViewportSpecificProtocolOptions,
DisplaySetAndViewportOptions
};
+5
View File
@@ -20,10 +20,12 @@ import progressTrackingUtils from './progressTrackingUtils';
import isLowPriorityModality from './isLowPriorityModality';
import { isImage } from './isImage';
import isDisplaySetReconstructable from './isDisplaySetReconstructable';
import sortInstancesByPosition from './sortInstancesByPosition';
import imageIdToURI from './imageIdToURI';
import debounce from './debounce';
import roundNumber from './roundNumber';
import downloadCSVReport from './downloadCSVReport';
import isEqualWithin from './isEqualWithin';
// Commented out unused functionality.
// Need to implement new mechanism for derived displaySets using the displaySetManager.
@@ -46,6 +48,7 @@ const utils = {
hotkeys,
Queue,
isDicomUid,
isEqualWithin,
resolveObjectPath,
hierarchicalListUtils,
progressTrackingUtils,
@@ -72,12 +75,14 @@ export {
hotkeys,
Queue,
isDicomUid,
isEqualWithin,
resolveObjectPath,
hierarchicalListUtils,
progressTrackingUtils,
isLowPriorityModality,
isImage,
isDisplaySetReconstructable,
sortInstancesByPosition,
imageIdToURI,
debounce,
roundNumber,
+1
View File
@@ -17,6 +17,7 @@ describe('Top level exports', () => {
'b64toBlob',
'formatDate',
'formatPN',
'isEqualWithin',
//'loadAndCacheDerivedDisplaySets',
'isDisplaySetReconstructable',
'isImage',
@@ -1,4 +1,5 @@
import toNumber from './toNumber';
import sortInstancesByPosition from './sortInstancesByPosition';
// TODO: Is 10% a reasonable spacingTolerance for spacing?
const spacingTolerance = 0.2;
@@ -28,10 +29,12 @@ export default function isDisplaySetReconstructable(instances) {
return { value: false };
}
const sortedInstances = sortInstancesByPosition(instances);
if (isMultiframe) {
return processMultiframe(instances[0]);
return processMultiframe(sortedInstances[0]);
} else {
return processSingleframe(instances);
return processSingleframe(sortedInstances);
}
}
+27
View File
@@ -0,0 +1,27 @@
/**
* returns equal if the two arrays are identical within the
* given tolerance.
*
* @param v1 - The first array of values
* @param v2 - The second array of values.
* @param tolerance - The acceptable tolerance, the default is 0.00001
*
* @returns True if the two values are within the tolerance levels.
*/
export default function isEqualWithin(
v1: number[] | Float32Array,
v2: number[] | Float32Array,
tolerance = 1e-5
): boolean {
if (v1.length !== v2.length) {
return false;
}
for (let i = 0; i < v1.length; i++) {
if (Math.abs(v1[i] - v2[i]) > tolerance) {
return false;
}
}
return true;
}
@@ -0,0 +1,61 @@
import sortInstances from './sortInstancesByPosition';
describe('sortInstances', () => {
it('should sort instances based on their imagePositionPatient', () => {
const instances = [
{
ImagePositionPatient: [0, 0, 2],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
{
ImagePositionPatient: [0, 0, 1],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
{
ImagePositionPatient: [0, 0, 0],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
];
const sortedInstances = sortInstances(instances);
expect(sortedInstances).toEqual([
{
ImagePositionPatient: [0, 0, 0],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
{
ImagePositionPatient: [0, 0, 1],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
{
ImagePositionPatient: [0, 0, 2],
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
},
]);
});
it('should return the same instances if there is only one instance', () => {
const instances = [
{
ImagePositionPatient: [0, 0, 0],
},
];
const sortedInstances = sortInstances(instances);
expect(sortedInstances).toEqual([
{
ImagePositionPatient: [0, 0, 0],
},
]);
});
it('should return the same instances if there are no instances', () => {
const instances = [];
const sortedInstances = sortInstances(instances);
expect(sortedInstances).toEqual([]);
});
});
@@ -0,0 +1,67 @@
import { vec3 } from 'gl-matrix';
/**
* Given an array of imageIds, sort them based on their imagePositionPatient, and
* also returns the spacing between images and the origin of the reference image
*
* @param imageIds - array of imageIds
* @param scanAxisNormal - [x, y, z] array or gl-matrix vec3
*
* @returns The sortedImageIds, zSpacing, and origin of the first image in the series.
*/
export default function sortInstances(instances: Array<any>) {
// Return if only one instance e.g., multiframe
if (instances.length <= 1) {
return instances;
}
const {
ImagePositionPatient: referenceImagePositionPatient,
ImageOrientationPatient,
} = instances[Math.floor(instances.length / 2)]; // this prevents getting scout image as test image
const rowCosineVec = vec3.fromValues(
ImageOrientationPatient[0],
ImageOrientationPatient[1],
ImageOrientationPatient[2]
);
const colCosineVec = vec3.fromValues(
ImageOrientationPatient[3],
ImageOrientationPatient[4],
ImageOrientationPatient[5]
);
const scanAxisNormal = vec3.cross(vec3.create(), rowCosineVec, colCosineVec);
const refIppVec = vec3.set(
vec3.create(),
referenceImagePositionPatient[0],
referenceImagePositionPatient[1],
referenceImagePositionPatient[2]
);
const distanceInstancePairs = instances.map(instance => {
const imagePositionPatient = instance.ImagePositionPatient;
const positionVector = vec3.create();
vec3.sub(
positionVector,
referenceImagePositionPatient,
imagePositionPatient
);
const distance = vec3.dot(positionVector, scanAxisNormal);
return {
distance,
instance,
};
});
distanceInstancePairs.sort((a, b) => b.distance - a.distance);
const sortedInstances = distanceInstancePairs.map(a => a.instance);
return sortedInstances;
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Given the flatten index, and rows and column, it returns the
* row and column index
*/
const unravelIndex = (index, numRows, numCols) => {
const row = Math.floor(index / numCols);
const col = index % numCols;
return { row, col };
};
export default unravelIndex;
+13
View File
@@ -0,0 +1,13 @@
// prettier-ignore
// @ts-nocheck
/**
* Generates a unique id that has limited chance of collision
*
* @see {@link https://stackoverflow.com/a/2117523/1867984|StackOverflow: Source}
* @returns a v4 compliant GUID
*/
export default function uuidv4(): string {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

@@ -1,9 +1,9 @@
---
sidebar_position: 1
sidebar_label: Overview
sidebar_label: Configuration Files
---
# Overview
# Config files
After following the steps outlined in
[Getting Started](./../development/getting-started.md), you'll notice that the
@@ -105,6 +105,21 @@ window.config = ({ servicesManager } = {}) => {
};
```
## Configuration Options
Here are a list of some options available:
- `maxNumberOfWebWorkers`: The maximum number of web workers to use for
decoding. Defaults to minimum of `navigator.hardwareConcurrency` and
what is specified by `maxNumberOfWebWorkers`. Some windows machines require smaller values.
- `omitQuotationForMultipartRequest`: Some servers (e.g., .NET) require the `multipart/related` request to be sent without quotation marks. Defaults to `false`. If your server doesn't require this, then setting this flag to `true` might improve performance (by removing the need for preflight requests). Also note that
if auth headers are used, a preflight request is required.
- `maxNumRequests`: The maximum number of requests to allow in parallel. It is an object with keys of `interaction`, `thumbnail`, and `prefetch`. You can specify a specific number for each type.
- `showLoadingIndicator`: (default to true), if set to false, the loading indicator will not be shown when navigating between studies.
<!-- **Embedded Use Note:**
Alternatively, when using the `umd` bundle for embedded use cases, these same
@@ -1,5 +1,5 @@
---
sidebar_position: 1
sidebar_position: 2
sidebar_label: DICOMweb
---
@@ -0,0 +1,19 @@
---
sidebar_position: 1
sidebar_label: Introduction
---
# Data Source
The internal data structure of OHIF’s metadata follows naturalized DICOM JSON, A
format pioneered by `dcmjs`. In short DICOM metadata headers with DICOM Keywords
instead of tags and sequences as arrays, for easy development and clear code.
Here in this section we will discuss couple of data sources that are commonly used
and OHIF has provided the implementation for them.
## Custom Data Source
Do you have a custom data source? or a custom data that you want to use in OHIF?
You can easily write a data source to map your data to OHIF’s native format.
You can read more in the [Data Source Module](../../platform/extensions/modules/data-source.md)
@@ -1,5 +1,5 @@
---
sidebar_position: 2
sidebar_position: 4
sidebar_label: Static Files
---
+9
View File
@@ -0,0 +1,9 @@
---
sidebar_position: 3
sidebar_label: URL
---
# URL
URL
@@ -7,11 +7,7 @@ sidebar_label: Data Source
## Overview
The internal data structure of OHIF’s metadata follows naturalized DICOM JSON, A
format pioneered by `dcmjs`. In short DICOM metadata headers with DICOM Keywords
instead of tags and sequences as arrays, for easy development and clear code.
We have built a standard for fetching and mapping data into OHIF’s native
We have built couple of methods for fetching and mapping data into OHIF’s native
format, which we call DataSources, and have provided one implementation of this
standard.
@@ -5,24 +5,105 @@ sidebar_label: Hanging Protocol
# Module: Hanging Protocol
## Overview
`hangingProtocolModule` provides the protocols for hanging the displaySets in the viewer.
This module can be as simple as loading a list of pre-defined protocols, or it can be more complex
and `fetch` the protocols from a server.
You can read more about hanging protocols in HangingProtocolService.
[Hanging protocols](http://dicom.nema.org/dicom/Conf-2005/Day-2_Selected_Papers/B305_Morgan_HangProto_v1.pdf) are an essential part of any radiology viewer.
OHIF uses Hanging Protocols to handle the arrangement of the images in the viewport. In
short, the registered protocols will get matched with the DisplaySets that are
available. Each protocol gets a score, and they are ranked. The
winning protocol (highest score) gets applied and its settings run for the viewports
to be arranged.
In `OHIF-v3` hanging protocols you can:
- Define what layout the viewport should starts with (e.g., 2x2 layout)
- Specify the type of the viewport and its orientation (e.g., stack, volume with Sagittal view)
- Define which displaySets gets displayed in which viewport of the layout (e.g,. displaySet that has modality of 'CT' and 'SeriesDescription' of 'Coronary Arteries' gets displayed in the first viewport of the layout)
- Apply certain initial viewport settings (e.g., inverting the contrast, jumping to a specific slice, etc.)
- Add specific synchronization rules for the viewports (e.g., synchronize the zoom of the viewports of the index 1, 2 OR synchronize the VOI of the viewports of the index 2, 3)
Using `hangingProtocolModule` you can provide/register the protocols for OHIF to
utilize.
Here is an example protocol which if used will hang a 1x3 layout with the first viewport showing a CT image, the second viewport showing a PT image and the third viewport showing their fusion, all in Sagittal orientations to achieve a view of
![](../../../assets/img/hangingProtocolExample.png)
```js
const deafultProtocol = {
id: 'defaultProtocol',
const oneByThreeProtocol = {
id: 'oneByThreeProtocol',
locked: true,
hasUpdatedPriorsInformation: false,
name: 'Default',
createdDate: '2021-02-23T19:22:08.894Z',
modifiedDate: '2021-02-23T19:22:08.894Z',
modifiedDate: '2022-10-04T19:22:08.894Z',
availableTo: {},
editableBy: {},
protocolMatchingRules: [],
toolGroupIds: ['default'],
imageLoadStrategy: 'interleaveTopToBottom',
protocolMatchingRules: [
{
attribute: 'ModalitiesInStudy',
constraint: {
contains: ['CT', 'PT'],
},
},
],
displaySetSelectors: {
ctDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'Modality',
constraint: {
equals: {
value: 'CT',
},
},
required: true,
},
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
ptDisplaySet: {
seriesMatchingRules: [
{
attribute: 'Modality',
constraint: {
equals: 'PT',
},
required: true,
},
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
{
attribute: 'SeriesDescription',
constraint: {
contains: 'Corrected',
},
},
],
},
},
stages: [
{
id: 'hYbmMy3b7pz7GLiaT',
@@ -31,164 +112,26 @@ const deafultProtocol = {
layoutType: 'grid',
properties: {
rows: 1,
columns: 2,
columns: 3,
},
},
displaySets: [
{
id: 'displaySet',
imageMatchingRules: [],
seriesMatchingRules: [],
studyMatchingRules: [],
},
],
viewports: [
{
viewportOptions: {
toolGroupId: 'default',
},
displaySets: [
{
options: [],
id: 'displaySet',
viewportId: 'ctAXIAL',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
],
},
],
createdDate: '2021-02-23T18:32:42.850Z',
},
],
numberOfPriorsReferenced: -1,
};
function getHangingProtocolModule() {
return [
{
id: 'default',
protocol: deafultProtocol,
},
];
}
```
## Skeleton of A Hanging Protocol
The skeleton of a hanging protocol is as follows:
```js
const testProtocol = {
id: 'test',
locked: true,
hasUpdatedPriorsInformation: false,
name: 'Default',
createdDate: '2021-02-23T19:22:08.894Z',
modifiedDate: '2021-02-23T19:22:08.894Z',
availableTo: {},
editableBy: {},
toolGroupIds: ['default'],
protocolMatchingRules: [
{
id: 'wauZK2QNEfDPwcAQo',
weight: 1,
attribute: 'StudyDescription',
constraint: {
contains: {
value: 'PETCT',
},
},
required: false,
},
{
id: 'wauZK2QNEfDPwcAQo',
weight: 1,
attribute: 'StudyDescription',
constraint: {
contains: {
value: 'PET/CT',
},
},
required: false,
},
],
stages: [
{
id: 'hYbmMy3b7pz7GLiaT',
name: 'default',
viewportStructure: {
type: 'grid',
properties: {
rows: 1,
columns: 1,
viewportOptions: [],
},
},
displaySets: [
{
id: 'ctDisplaySet',
imageMatchingRules: [],
seriesMatchingRules: [
{
id: 'GPEYqFLv2dwzCM322',
weight: 1,
attribute: 'Modality',
constraint: {
equals: {
value: 'CT',
},
syncGroups: [
{
type: 'voi',
id: 'ctWLSync',
source: true,
target: true,
},
required: true,
},
],
studyMatchingRules: [],
},
{
id: 'ptACDisplaySet',
imageMatchingRules: [],
seriesMatchingRules: [
{
id: 'GPEYqFLv2dwzCM322',
weight: 1,
attribute: 'Modality',
constraint: {
equals: {
value: 'PT',
},
},
required: true,
},
],
studyMatchingRules: [],
},
{
id: 'ptNACDisplaySet',
imageMatchingRules: [],
seriesMatchingRules: [
{
id: 'GPEYqFLv2dwzCM322',
weight: 1,
attribute: 'SeriesDescription',
constraint: {
contains: {
value: 'Uncorrected',
},
},
required: false,
},
],
studyMatchingRules: [],
},
],
viewports: [
{
viewportOptions: {
viewportId: 'ctAxial',
viewportType: 'stack',
background: [0, 0, 0],
orientation: 'AXIAL',
toolGroupId: 'default',
],
},
displaySets: [
{
@@ -198,22 +141,49 @@ const testProtocol = {
},
{
viewportOptions: {
viewportId: 'ptAxial',
viewportType: 'stack',
background: [1, 1, 1],
orientation: 'AXIAL',
toolGroupId: 'default',
viewportId: 'ptAXIAL',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
},
displaySets: [
{
id: 'ptDisplaySet',
},
],
},
{
viewportOptions: {
viewportId: 'fusionSAGITTAL',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'ctWLSync',
source: false,
target: true,
},
],
},
displaySets: [
{
id: 'ctDisplaySet',
},
{
options: {
colormap: 'hsv',
voi: {
windowWidth: 5,
windowCenter: 2.5,
},
voiInverted: true,
},
id: 'ptACDisplaySet',
id: 'ptDisplaySet',
},
],
},
@@ -223,12 +193,31 @@ const testProtocol = {
],
numberOfPriorsReferenced: -1,
};
function getHangingProtocolModule() {
return [
{
id: 'oneByThreeProtocol',
protocol: oneByThreeProtocol,
},
];
}
```
Let's discuss each property in depth.
## Skeleton of a Protocol
The skeleton of a hanging protocol is as follows:
### Id
unique identifier for the protocol
unique identifier for the protocol, this id can be used inside mode configuration
to specify which protocol should be used for a specific mode. A mode can
request a protocol by its id (which makes OHIF to apply the protocol without
matching), or provides and array of ids which will
make the ProtocolEngine to choose the best matching protocol (based on
protocolMatching rules, which is next section).
### protocolMatchingRules
A list of criteria for the protocol along with the provided points for ranking.
@@ -237,8 +226,12 @@ A list of criteria for the protocol along with the provided points for ranking.
protocols get sorted based on the weights, and the winning protocol gets
applied to the viewer.
- `attribute`: tag that needs to be matched against. This can be either
Study-level metadata or a custom attribute.
[Learn more about custom attribute matching](#custom-attribute)
Study-level metadata or a custom attribute such as "StudyInstanceUID",
"StudyDescription", "ModalitiesInStudy", "NumberOfStudyRelatedSeries", "NumberOfSeriesRelatedInstances"
In addition to these tags, you can also use a custom attribute that you have registered before.
We will learn more about this later.
- `constraint`: the constraint that needs to be satisfied for the attribute. It accepts a `validator` which can be
[`equals`, `doesNotEqual`, `contains`, `doesNotContain`, `startsWith`, `endsWidth`]
@@ -259,9 +252,74 @@ A list of criteria for the protocol along with the provided points for ranking.
},
```
### displaySetSelectors
Defines the display sets that the protocol will use for arrangement.
```js
displaySetSelectors: {
ctDisplaySet: {
seriesMatchingRules: [
{
weight: 1,
attribute: 'Modality',
constraint: {
equals: {
value: 'CT',
},
},
required: true,
},
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
],
},
ptDisplaySet: {
seriesMatchingRules: [
{
attribute: 'Modality',
constraint: {
equals: 'PT',
},
required: true,
},
{
weight: 1,
attribute: 'isReconstructable',
constraint: {
equals: {
value: true,
},
},
required: true,
},
{
attribute: 'SeriesDescription',
constraint: {
contains: 'Corrected',
},
},
],
},
}
```
As you see above we have specified two displaysets: 1) ctDisplaySet , 2) ptDisplaySet
The ctDisplaySet will match against all the series that are CT and reconstructable
The ptDisplaySet will match against all the series that are PT and reconstructable.
As you see each selector is composed of an `id` as the key and a set of `seriesMatchingRules` (displaySetMatchingRules) which gives score to the displaySet
based on the matching rules. The displaySet with the highest score will be used for the `id`.
### stages
Each protocol can define one or more stages. Each stage defines a certain layout and viewport rules.
Therefore, the `stages` property is array of objects, each object being one stage.
Each protocol can define one or more stages. Each stage defines a certain layout and viewport rules. Therefore, the `stages` property is array of objects, each object being one stage.
### viewportStructure
Defines the layout of the viewer. You can define the number of `rows` and `columns`. There should be `rows * columns` number of
@@ -304,88 +362,6 @@ viewportStructure: {
},
```
### displaySets
Defines the display sets that are available for the stage. Each
include an `id` and some `seriesMatchingRules`
```js
displaySets: [
{
id: 'ctDisplaySet',
imageMatchingRules: [],
seriesMatchingRules: [
{
id: 'GPEYqFLv2dwzCM322',
weight: 1,
attribute: 'Modality',
constraint: {
equals: {
value: 'CT',
},
},
required: true,
},
],
studyMatchingRules: [],
},
{
id: 'ptACDisplaySet',
imageMatchingRules: [],
seriesMatchingRules: [
{
id: 'GPEYqFLv2dwzCM322',
weight: 1,
attribute: 'Modality',
constraint: {
equals: {
value: 'PT',
},
},
required: true,
},
],
studyMatchingRules: [],
},
{
id: 'ptNACDisplaySet',
imageMatchingRules: [],
seriesMatchingRules: [
{
id: 'GPEYqFLv2dwzCM322',
weight: 1,
attribute: 'Modality',
constraint: {
equals: {
value: 'PT',
},
},
required: true,
},
{
id: 'GPEYqFLv2dwzCM322',
weight: 1,
attribute: 'SeriesDescription',
constraint: {
contains: {
value: 'Uncorrected',
},
},
required: false,
},
],
studyMatchingRules: [],
},
],
```
As you see in the above `displaySets` entry in the example above, there are
three defined displaySets that __will__ become available later on in the `viewports`
section of the protocol.
1. `ctDisplaySet` which is matched against the CT modality
2. `ptACDisplaySet` which is matched against the PT modality
3. `ptNACDisplaySet` which is matched against the PT modality and also contains the `Uncorrected` series description
### viewports
This field includes the viewports that will get hung on the viewer.
@@ -394,11 +370,20 @@ This field includes the viewports that will get hung on the viewer.
viewports: [
{
viewportOptions: {
viewportId: 'ctAxial',
viewportType: 'stack',
background: [0, 0, 0],
orientation: 'AXIAL',
toolGroupId: 'default',
viewportId: 'ctAXIAL',
viewportType: 'volume',
orientation: 'sagittal',
initialImageOptions: {
preset: 'middle',
},
syncGroups: [
{
type: 'voi',
id: 'ctWLSync',
source: true,
target: true,
},
],
},
displaySets: [
{
@@ -406,44 +391,78 @@ viewports: [
},
],
},
{
viewportOptions: {
viewportId: 'ptAxial',
viewportType: 'stack',
background: [1, 1, 1],
orientation: 'AXIAL',
toolGroupId: 'default',
},
displaySets: [
{
options: {
voi: {
windowWidth: 5,
windowCenter: 2.5,
},
voiInverted: true,
},
id: 'ptACDisplaySet',
},
],
},
// the rest
],
```
As you can see there are two viewports defined. The first one is a CT viewport and the second one is a PT viewport.
Each viewport has two properties:
As you can see in the hanging protocol we defined three viewports (but only showing one of them right above). Each viewport has two properties:
1. `viewportOptions`: defines the viewport properties such as
- `viewportId`: unique identifier for the viewport (optional)
- `viewportType`: type of the viewport (required - currently only stack)
- `viewportType`: type of the viewport (optional - options: stack, volume - default is stack
- `background`: background color of the viewport (optional)
- `orientation`: orientation of the viewport (optional)
- `orientation`: orientation of the viewport (optional - if not defined for volume -> acquisition axis)
- `toolGroupId`: tool group that will be used for the viewport (optional)
- `initialImageOptions`: initial image options (optional - can be specific imageIndex number or preset (first, middle, last))
- `syncGroups`: sync groups for the viewport (optional)
2. `displaySets`: defines the display sets that are available for the viewport. It is an array of objects, each object being one display set.
2. `displaySets`: defines the display sets that are displayed on a viewport. It is an array of objects, each object being one display set.
- `id`: id of the display set (required)
- `options` (optional): options for the display set
- voi: windowing options for the display set
- voi: windowing options for the display set (optional: windowWidth, windowCenter)
- voiInverted: whether the VOI is inverted or not (optional)
- colormap: colormap for the display set (optional: 'hsv' etc.)
### Custom attribute
For any matching rules you can specify a custom attribute too. For instance,
if you have a timepoint attribute in for each of your studies, you can use that in the matching rules.
```js
{
id: 'wauZK2QNEfDPwcAQo',
weight: 1,
attribute: 'timepoint',
constraint: {
equals: {
value: 'baseline',
},
},
required: false,
},
```
and then you need to register a callback in the HangingProtocolService to get the value for the attribute.
```js
HangingProtocolService.addCustomAttribute(
'timepoint', // attributeId
'addCustomAttribute', // attributeName
study => { // callback that returns the value for the attribute
const timePoint = fetchFromMyCustomBackend(study.studyInstanceUid);
return timePoint;
}
);
```
## Matching on Prior Study with UID
Often it is desired to match a new study to a prior study (e.g., follow up on
a surgery). Since the hanging protocols run on displaySets we need to have a
way to let OHIF knows that it needs to load the prior study as well. This can
be done by specifying both StudyInstanceUIDs in the URL. Below we are
running OHIF with two studies
```bash
http://localhost:3000/viewer?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5&StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125095722.1
```
Now you have access to both studies and you can use matchingRules to match
displaySets.
Our roadmap includes enabling matching on prior studies without the UID (e.g., baseline, most recent and index).
+6 -2
View File
@@ -298,8 +298,12 @@ properties inside the [route documentation](./routes.md)
### HangingProtocols
Currently, you can pass your defined hanging protocols inside the
`hangingProtocols` property of the mode's config. This will get registered
inside `HangingProtocolService`.
`hangingProtocols` property of the mode's config. If you specify the hanging protocol
explicitly by its name (only string and not array), it will be THE hanging protocol
that the mode runs with. However, if you specify an array of hanging protocols,
they will get ranked based on the displaySetSelector requirements and the winner
will be the hanging protocol that the mode runs with.
### SopClassHandlers
@@ -7,33 +7,25 @@ sidebar_label: Hanging Protocol Service
## Overview
`HangingProtocolService` is a migration of the `OHIF-v1` hanging protocol
engine along with various improvements and fixes.
This service handles the arrangement of the images in the viewport. In
short, the registered protocols will get matched with the DisplaySets that are
available. Each protocol gets a score, and they are ranked. The
winning protocol gets applied and its settings run for the viewports.
winning protocol (highest score) gets applied and its settings run for the viewports
to be arranged.
You can read more about hanging protocols
[here](http://dicom.nema.org/dicom/Conf-2005/Day-2_Selected_Papers/B305_Morgan_HangProto_v1.pdf).
In `OHIF-v3` hanging protocols you can:
You can read more about a HangingProtocol Structure and its properties in the
[HangingProtocol Module](../../extensions/modules/hpModule.md).
- Define what layout of the viewport should the viewer starts with (2x2 layout)
- Specify the type of the viewport and its orientation
- Define which displaySets gets displayed in which viewport of the layout
- Apply certain initial viewport settings; e.g., inverting the contrast, jumping to a specific slice, etc.
- Add specific synchronization rules for the viewports
The rest of this page is dedicated on how the Hanging Protocol Service works and
what you can do with it.
## Protocols
A protocol can be an object or a function that returns an object (protocol generator).
Each protocol can get added to the `HangingProtocolService` by using the `addProtocol` method given
an `id` and the protocol itself. As an example, the `default` protocol is an object, while
the 'mpr' protocol is a function that returns the protocol (the reason for this is that the
`mpr` protocol needs to be generated based on the active viewport).
Protocols are provided by each extension's HangingProtocolModule and are
registered automatically to the HangingProtocolService.
All protocols are stored in the `HangingProtocolService` using their `id` as the key, and
the protocol itself as the value.
All protocols are stored in the `HangingProtocolService` using their `id` as the key, and the protocol itself as the value.
## Events
@@ -44,14 +36,15 @@ There are two events that get publish in `HangingProtocolService`:
| NEW_LAYOUT | Fires when a new layout is requested by the `HangingProtocolService` |
| STAGE_CHANGE | Fires when the the stage is changed in the hanging protocols |
| PROTOCOL_CHANGED | Fires when the the protocol is changed in the hanging protocols |
| HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT | Fires when the hanging protocol applies for a viewport (sets its displaySets) |
## API
- `getMatchDetails`: returns an object which contains the details of the
matching for the viewports, displaysets and whether the protocol is
applied to the viewport or not
matching for the viewports, displaySets and whether the protocol is
applied to the viewport or not yet.
- `addProtocol`: adds provided protocol to the list of registered protocols
for matching
@@ -68,6 +61,14 @@ init.
- `addCustomAttribute`: adding a custom attribute for matching. (see below)
- `setProtocol`: applies a protocol to the current studies, it can be used for instance to apply a
hanging protocol when pressing a button in the toolbar. We use this for applying 'mpr'
hanging protocol when pressing the MPR button in the toolbar. `setProtocol` will
accept a set of options that can be used to define the displaySets that will be
used for the protocol. If no options are provided, all displaySets will
be used to match the protocol.
Default initialization of the modes handles running the `HangingProtocolService`
## Hanging Protocol Instance Definition
@@ -0,0 +1,55 @@
---
sidebar_position: 7
sidebar_label: Segmentation Service
---
# Segmentation Service
## Overview
Using Segmentation Service you can create, edit and delete segmentation data, and
change appearance of the segmentation including color, opacity, and visibility.
Segmentations in OHIF are based on the Segmentations in Cornerstone3D. You can
read more about it in the [Cornerstone Segmentation](https://www.cornerstonejs.org/docs/concepts/cornerstone-tools/segmentation/). OHIF currently only supports
one representation of the segmentation data.
## Events
There are seven events that get publish in `MeasurementService`:
| Event | Description |
| --------------------- | ------------------------------------------------------ |
| SEGMENTATION_UPDATED | Fires when a segmentation is updated e.g., segment added, removed etc.|
| SEGMENTATION_DATA_MODIFIED | Fires when the segmentation data changes |
| SEGMENTATION_ADDED | Fires when a new segmentation is added to OHIF |
| SEGMENTATION_REMOVED | Fires when a segmentation is removed from OHIF |
| SEGMENTATION_CONFIGURATION_CHANGED | Fires when a segmentation configuration is changed |
| SEGMENT_PIXEL_DATA_CREATED | Fires when a segment group adds its pixel data to the volume |
| SEGMENTATION_PIXEL_DATA_CREATED | Fires when the full segmentation volume is filled with its segments |
## API
### Segmentation Creation
- `createSegmentationForDisplaySet`: based on a reference displaySet, create a new segmentation. E.g., create a new segmentation based on a CT series
- `createSegmentationForSEGDisplaySet`: given a segDisplaySet loaded by a sopClassHandler, create a new segmentation
- `addSegmentationRepresentationToToolGroup`: given the toolGroupId, add the given segmentationId to the toolGroup.
### Segmentation Behavior
- setActiveSegmentationForToolGroup, getSegmentations, getSegmentation, jumpToSegmentCenter, highlightSegment
### Segment Behavior
- setSegmentLockedForSegmentation, removeSegment, addSegment, setSegmentLockedForSegmentation, setSegmentLabel, setActiveSegmentForSegmentation,
setSegmentRGBAColorForSegmentation
### Segmentation Configuration
Setters
- setSegmentVisibility, setSegmentColor, setSegmentRGBA, setSegmentOpacity, toggleSegmentationVisibility,
@@ -59,6 +59,17 @@ The following services is available in the `OHIF-v3`.
DisplaySetService
</td>
</tr>
<tr>
<td>
<a href="./data/SegmentationService">
SegmentationService (NEW)
</a>
</td>
<td>Segmentation Service</td>
<td>
SegmentationService
</td>
</tr>
<tr>
<td>
<a href="./data/HangingProtocolService">
+1 -1
View File
@@ -233,7 +233,7 @@ module.exports = {
// : undefined,
versions: {
current: {
label: 'Version 3.2 - Cornerstone3D Volume API 🚧',
label: 'Version 3.3 - Segmentation Support 🚧',
},
'2.0': {
label: 'Version 2.0 - Master branch',
+1 -1
View File
@@ -284,7 +284,7 @@ module.exports = {
},
fontFamily: {
sans: [
'Lato',
'Inter',
'system-ui',
'-apple-system',
'BlinkMacSystemFont',
@@ -71,7 +71,7 @@ function TrackedCornerstoneViewport({
return (
<>
<ViewportActionBar onDoubleClick onSeriesChange />
<ViewportActionBar onDoubleClick onArrowsClick />
<div className="relative flex flex-row w-full h-full overflow-hidden">
{renderViewport()}
</div>
+1
View File
@@ -1,4 +1,5 @@
{
"Version 3.2 - Cornerstone3D VolumeAPI": "https://deploy-preview-3009--ohif-platform-docs.netlify.app/",
"Version 3.1 - Cornerstone3D Basics": "https://62e92bf05fc5050008aa1300--ohif-platform-docs.netlify.app/",
"Version 3.0 - Cornerstone Legacy": "https://deploy-preview-2791--ohif-platform-docs.netlify.app/",
"Version 1.0": "https://deploy-preview-2791--ohif-platform-docs.netlify.app/1.0/"
+3 -1
View File
@@ -43,7 +43,9 @@
"react-modal": "3.11.2",
"react-outside-click-handler": "^1.3.0",
"react-select": "3.0.8",
"react-with-direction": "^1.3.1"
"react-with-direction": "^1.3.1",
"lottie-react": "^2.3.1",
"swiper": "^8.4.2"
},
"devDependencies": {
"@babel/core": "^7.14.8",
@@ -0,0 +1,9 @@
<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path id="bf9k9cj5la" d="m3 7.463.994-.982 2.131 2.113L10.756 4l.994.987-5.625 5.575z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<rect stroke="#5ACCE6" fill="#5ACCE6" x=".5" y=".5" width="14" height="14" rx="3"/>
<use fill="#000" xlink:href="#bf9k9cj5la"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 447 B

@@ -0,0 +1,9 @@
<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path id="3nvolf8jsa" d="M4.9 8.45 2.4 5.97l.795-.785L4.9 6.875 8.605 3.2l.795.79z"/>
</defs>
<g fill="none" fill-rule="evenodd">
<rect stroke="#348CFD" fill="#348CFD" x=".5" y=".5" width="11" height="11" rx="3"/>
<use fill="#000" xlink:href="#3nvolf8jsa"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 444 B

@@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg">
<rect x="807.5" y="496.5" width="14" height="14" rx="3" transform="translate(-807 -496)" fill="#000" stroke="#5ACCE6" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 235 B

@@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
<rect x=".5" y=".5" width="11" height="11" rx="3" stroke="#348CFD" fill="none" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 196 B

@@ -0,0 +1,9 @@
<svg width="19" height="19" viewBox="0 0 19 19" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="currentColor" fill-rule="evenodd">
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="m9.75.5-8.5 3.719 8.5 3.718 8.5-3.718z"/>
<path d="M1.25 4.219v10.093l8.5 3.72 8.5-3.72V4.22M9.75 7.938v10.093"/>
</g>
<path fill="currentColor" d="M1.25 4.576v9.736l8.05 3.72.45-10.095z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 482 B

@@ -0,0 +1,6 @@
<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(1 1)" fill="none" fill-rule="evenodd">
<circle stroke="#1C498E" stroke-width=".5" cx="7.5" cy="7.5" r="7.5"/>
<path fill="#1C498E" fill-rule="nonzero" d="M6.504 3 5.5 4.004l3.259 3.259L5.5 10.52l1.004 1.004 4.263-4.262z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 367 B

@@ -0,0 +1,6 @@
<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(1 1)" fill="none" fill-rule="evenodd">
<circle stroke="#348CFD" stroke-width=".5" cx="7.5" cy="7.5" r="7.5"/>
<path fill="#348CFD" fill-rule="nonzero" d="M6.504 3 5.5 4.004l3.259 3.259L5.5 10.52l1.004 1.004 4.263-4.262z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 367 B

@@ -0,0 +1,6 @@
<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(-1 0 0 1 16 1)" fill="none" fill-rule="evenodd">
<circle stroke="#1C498E" stroke-width=".5" cx="7.5" cy="7.5" r="7.5"/>
<path fill="#1C498E" fill-rule="nonzero" d="M6.504 3 5.5 4.004l3.259 3.259L5.5 10.52l1.004 1.004 4.263-4.262z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 374 B

@@ -0,0 +1,6 @@
<svg width="17" height="17" viewBox="0 0 17 17" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(-1 0 0 1 16 1)" fill="none" fill-rule="evenodd">
<circle stroke="#348CFD" stroke-width=".5" cx="7.5" cy="7.5" r="7.5"/>
<path fill="#348CFD" fill-rule="nonzero" d="M6.504 3 5.5 4.004l3.259 3.259L5.5 10.52l1.004 1.004 4.263-4.262z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 374 B

@@ -0,0 +1,7 @@
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg">
<g fill="#348CFD" fill-rule="evenodd">
<path d="M7 .102A6.899 6.899 0 1 0 7 13.9 6.899 6.899 0 0 0 7 .102zm0 .875a6.024 6.024 0 1 1 0 12.048A6.024 6.024 0 0 1 7 .977z"/>
<path d="M6.462 5.486c.503 0 .917.38.97.87l.006.106v3.769a.438.438 0 0 1-.868.078l-.007-.078V6.46a.101.101 0 0 0-.07-.095l-.031-.005H5.385a.437.437 0 0 1-.079-.868l.079-.007h1.077zM6.192 2.793l.089.006a.707.707 0 1 1-.177 0l.088-.006z"/>
<path d="M8.615 9.794c.242 0 .438.224.438.5 0 .246-.155.45-.359.492l-.079.008h-3.23c-.242 0-.438-.224-.438-.5 0-.245.155-.45.359-.492l.079-.008h3.23z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 689 B

@@ -0,0 +1,5 @@
<svg width="47" height="47" viewBox="0 0 47 47" xmlns="http://www.w3.org/2000/svg">
<g fill="#FFF" fill-rule="nonzero">
<path d="M43.585 0H29.763a3.415 3.415 0 0 0-3.415 3.415v13.822a3.415 3.415 0 0 0 3.415 3.415h13.822A3.415 3.415 0 0 0 47 17.237V3.415A3.415 3.415 0 0 0 43.585 0zM29.763 1.366h13.822c1.132 0 2.05.917 2.05 2.049v13.822a2.049 2.049 0 0 1-2.05 2.049H29.763a2.049 2.049 0 0 1-2.049-2.05V3.416c0-1.132.918-2.05 2.05-2.05zM17.237 0H3.415A3.415 3.415 0 0 0 0 3.415v13.822a3.415 3.415 0 0 0 3.415 3.415h13.822a3.415 3.415 0 0 0 3.415-3.415V3.415A3.415 3.415 0 0 0 17.237 0zM3.415 1.366h13.822c1.131 0 2.049.917 2.049 2.049v13.822a2.049 2.049 0 0 1-2.05 2.049H3.416a2.049 2.049 0 0 1-2.05-2.05V3.416c0-1.132.918-2.05 2.05-2.05zM43.585 26.348H29.763a3.415 3.415 0 0 0-3.415 3.415v13.822A3.415 3.415 0 0 0 29.763 47h13.822A3.415 3.415 0 0 0 47 43.585V29.763a3.415 3.415 0 0 0-3.415-3.415zm-13.822 1.366h13.822c1.132 0 2.05.918 2.05 2.05v13.821a2.049 2.049 0 0 1-2.05 2.05H29.763a2.049 2.049 0 0 1-2.049-2.05V29.763c0-1.131.918-2.049 2.05-2.049zM17.237 26.348H3.415A3.415 3.415 0 0 0 0 29.763v13.822A3.415 3.415 0 0 0 3.415 47h13.822a3.415 3.415 0 0 0 3.415-3.415V29.763a3.415 3.415 0 0 0-3.415-3.415zM3.415 27.714h13.822c1.131 0 2.049.918 2.049 2.05v13.821a2.049 2.049 0 0 1-2.05 2.05H3.416a2.049 2.049 0 0 1-2.05-2.05V29.763c0-1.131.918-2.049 2.05-2.049z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,5 @@
<svg width="16" height="11" viewBox="0 0 16 11" xmlns="http://www.w3.org/2000/svg">
<g stroke="#348CFD" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 5.406h9.834M6.765 1.337l4.069 4.07-4.07 4.068M15.242 9.475V1.337"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 311 B

@@ -0,0 +1,5 @@
<svg width="17" height="10" viewBox="0 0 17 10" xmlns="http://www.w3.org/2000/svg">
<g stroke="#348CFD" stroke-width="1.5" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.856 5.078H1M5.078 1 1 5.078l4.078 4.078M15.582 9.156V1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 302 B

@@ -0,0 +1,6 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M5.25 9a1.75 1.75 0 1 0-.001 3.499A1.75 1.75 0 0 0 5.25 9zm10.5 0a1.75 1.75 0 1 0-.001 3.499A1.75 1.75 0 0 0 15.75 9zM10.5 9a1.75 1.75 0 1 0-.001 3.499A1.75 1.75 0 0 0 10.5 9z" fill="#348CFD" fill-rule="nonzero"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 405 B

@@ -0,0 +1,6 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path fill="#FFF" d="m13.837 11-6 4.837V6.163z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 232 B

@@ -0,0 +1,11 @@
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="reference-lines" stroke="currentColor" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(3.250000, 4.926395)">
<path d="M0.75,8.32360458 L2.75,8.32360458 C3.16421356,8.32360458 3.5,7.98781815 3.5,7.57360458 C3.5,7.15939102 3.16421356,6.82360458 2.75,6.82360458 L0.75,6.82360458 C0.335786438,6.82360458 0,7.15939102 0,7.57360458 C0,7.98781815 0.335786438,8.32360458 0.75,8.32360458 Z M5.75,8.32360458 L7.75,8.32360458 C8.16421356,8.32360458 8.5,7.98781815 8.5,7.57360458 C8.5,7.15939102 8.16421356,6.82360458 7.75,6.82360458 L5.75,6.82360458 C5.33578644,6.82360458 5,7.15939102 5,7.57360458 C5,7.98781815 5.33578644,8.32360458 5.75,8.32360458 Z M10.75,8.32360458 L12.75,8.32360458 C13.1642136,8.32360458 13.5,7.98781815 13.5,7.57360458 C13.5,7.15939102 13.1642136,6.82360458 12.75,6.82360458 L10.75,6.82360458 C10.3357864,6.82360458 10,7.15939102 10,7.57360458 C10,7.98781815 10.3357864,8.32360458 10.75,8.32360458 Z M15.75,8.32360458 L17.75,8.32360458 C18.1642136,8.32360458 18.5,7.98781815 18.5,7.57360458 C18.5,7.15939102 18.1642136,6.82360458 17.75,6.82360458 L15.75,6.82360458 C15.3357864,6.82360458 15,7.15939102 15,7.57360458 C15,7.98781815 15.3357864,8.32360458 15.75,8.32360458 Z" id="Line-7" fill="#000000" fill-rule="nonzero"></path>
<g id="Group-2" transform="translate(1.250000, 0.000000)" stroke="currentColor" stroke-width="1.5">
<path d="M0,5 L0,1 C-6.76353751e-17,0.44771525 0.44771525,1.01453063e-16 1,0 L15,0 C15.5522847,-1.01453063e-16 16,0.44771525 16,1 L16,5 L16,5" id="Path-7"></path>
<path d="M0,15 L0,11 C-6.76353751e-17,10.4477153 0.44771525,10 1,10 L15,10 C15.5522847,10 16,10.4477153 16,11 L16,15 L16,15" id="Path-7" transform="translate(8.000000, 12.500000) scale(1, -1) translate(-8.000000, -12.500000) "></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+8
View File
@@ -0,0 +1,8 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<g stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.25">
<path d="M10.5 5.5v10M15.5 10.5h-10"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 334 B

@@ -0,0 +1,9 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<g stroke="#FFF" stroke-linecap="round" stroke-linejoin="round">
<path d="m12.625 5.872 2.232 2.231-7.034 7.035-3.72 1.488 1.487-3.72 7.035-7.034z"/>
<path d="M14.753 4.5c.404 0 .808.154 1.116.462.308.309.462.713.462 1.117s-.154.808-.462 1.116L14.753 8.31 12.52 6.08l1.115-1.117a1.574 1.574 0 0 1 1.117-.462z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 531 B

@@ -0,0 +1,11 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M0 0h21v21H0z"/>
<path d="M17.29 10.6c-.676-.77-3.378-3.6-6.61-3.6-3.23 0-5.933 2.83-6.609 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#3D5871" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.68 8.75c.69 0 1.298.344 1.666.87l-2.84 2.84c-.52-.37-.859-.98-.859-1.668 0-1.128.91-2.042 2.033-2.042zM12.689 10.47a2.038 2.038 0 0 1-2.337 2.337z" fill="#3D5871"/>
<g stroke="#3D5871" stroke-linecap="round" stroke-linejoin="round">
<path d="M16.396 5 5.02 16.375"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 782 B

@@ -0,0 +1,11 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M0 0h21v21H0z"/>
<path d="M17.29 10.6c-.676-.77-3.378-3.6-6.61-3.6-3.23 0-5.933 2.83-6.609 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.68 8.75c.69 0 1.298.344 1.666.87l-2.84 2.84c-.52-.37-.859-.98-.859-1.668 0-1.128.91-2.042 2.033-2.042zM12.689 10.47a2.038 2.038 0 0 1-2.337 2.337z" fill="#348CFD"/>
<g stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round">
<path d="M16.396 5 5.02 16.375"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 782 B

@@ -0,0 +1,9 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<g transform="translate(4 7)">
<path d="M13.29 3.6C12.614 2.83 9.912 0 6.68 0 3.45 0 .747 2.83.071 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<ellipse fill="#FFF" cx="6.68" cy="3.792" rx="2.033" ry="2.042"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 552 B

@@ -0,0 +1,7 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M8 10.5V8.559A2.554 2.554 0 0 1 10.549 6a2.554 2.554 0 0 1 2.548 2.559v2.483" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.468 11c0-.552-.42-1-.938-1H7.437c-.517 0-.937.448-.937 1v5c0 .552.42 1 .937 1h6.093c.518 0 .938-.448.938-1v-5z" fill="#FFF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 479 B

@@ -0,0 +1,9 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<g transform="translate(4 7)">
<path d="M13.29 3.6C12.614 2.83 9.912 0 6.68 0 3.45 0 .747 2.83.071 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#348CFD" stroke-linecap="round" stroke-linejoin="round"/>
<ellipse fill="#348CFD" cx="6.68" cy="3.792" rx="2.033" ry="2.042"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 558 B

@@ -0,0 +1,11 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M0 0h21v21H0z"/>
<path d="M17.29 10.6c-.676-.77-3.378-3.6-6.61-3.6-3.23 0-5.933 2.83-6.609 3.6a.293.293 0 0 0 0 .383c.676.77 3.378 3.6 6.61 3.6 3.23 0 5.933-2.83 6.609-3.6a.293.293 0 0 0 0-.383z" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.68 8.75c.69 0 1.298.344 1.666.87l-2.84 2.84c-.52-.37-.859-.98-.859-1.668 0-1.128.91-2.042 2.033-2.042zM12.689 10.47a2.038 2.038 0 0 1-2.337 2.337z" fill="#FFF"/>
<g stroke="#FFF" stroke-linecap="round" stroke-linejoin="round">
<path d="M16.396 5 5.02 16.375"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 773 B

@@ -0,0 +1,7 @@
<svg width="21" height="21" viewBox="0 0 21 21" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M0 0h21v21H0z"/>
<path d="M16.014 7.559A2.554 2.554 0 0 0 13.466 5a2.554 2.554 0 0 0-2.549 2.559v2.483" stroke="#FFF" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.968 10.824c0-.553-.42-1-.938-1H5.937c-.517 0-.937.447-.937 1v5c0 .552.42 1 .937 1h6.093c.518 0 .938-.448.938-1v-5z" fill="#FFF"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 483 B

@@ -0,0 +1,8 @@
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<rect stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x="1.5" y="16.37" width="4.13" height="4.13" rx="1"/>
<rect stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" x="16.37" y="1.5" width="4.13" height="4.13" rx="1"/>
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M5.388 16.612 16.612 5.388"/>
<path d="M0 0h22v22H0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 565 B

@@ -0,0 +1,7 @@
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="M3.638 13.16a17.265 17.265 0 0 1 3.652-1.586c.565-.21.626-1.504.221-1.95-.584-.642-1.08-1.396-1.08-3.215a2.572 2.572 0 0 1 2.632-2.811 2.572 2.572 0 0 1 2.631 2.811c0 1.822-.495 2.573-1.08 3.215-.404.446-.343 1.74.222 1.95 1.275.386 2.5.919 3.652 1.587" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.15 11.064V2.369a.87.87 0 0 0-.87-.869H2.37a.87.87 0 0 0-.87.87v13.91c0 .48.39.87.87.87h8.694M15.846 20.628l-3.043.87.87-3.044 5.2-5.2a1.537 1.537 0 0 1 2.174 2.173l-5.201 5.2zM18.221 13.905l2.174 2.174M13.672 18.454l2.174 2.174" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M0 0h22v22H0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 837 B

@@ -0,0 +1,10 @@
<svg width="23" height="22" viewBox="0 0 23 22" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<path d="m13.34 11.74-2.971.425.425-2.971 7.64-7.64A1.8 1.8 0 1 1 20.98 4.1l-7.64 7.64zM8.644 19.034h7.383c.865 0 1.566-.701 1.566-1.566V10.03M12.504 4.94H5.066C4.2 4.94 3.5 5.643 3.5 6.508v1.972" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>
<path stroke="currentColor" d="m11.5 8.5 2.333 2.333"/>
<g stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path d="m1 14 2.5-2.5L6 14M1 17.5 3.5 20 6 17.5"/>
</g>
<path d="M0 0h22v22H0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 672 B

@@ -0,0 +1,11 @@
<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<circle stroke="currentColor" cx="11.037" cy="10.912" r="8"/>
<g stroke="currentColor">
<path stroke-linecap="square" d="m11.354 3.575-7.779 7.779M17.364 6.757 6.757 17.364"/>
<path d="m18.955 9.763-9.192 9.192"/>
<path stroke-linecap="square" d="M15.066 4.46 4.459 15.065"/>
</g>
<path d="M0 0h22v22H0z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 516 B

@@ -0,0 +1,3 @@
<svg width="6" height="5" viewBox="0 0 6 5" xmlns="http://www.w3.org/2000/svg">
<path d="M3 5 6 .5H0z" fill="#726F7E" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 151 B

@@ -0,0 +1,3 @@
<svg width="6" height="5" viewBox="0 0 6 5" xmlns="http://www.w3.org/2000/svg">
<path d="m3 0 3 4.5H0z" fill="#726F7E" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 152 B

+4 -4
View File
@@ -1,6 +1,6 @@
/* CUSTOM OHIF SCROLLBAR */
.ohif-scrollbar {
scrollbar-color: #3E5967 transparent;
scrollbar-color: #173239 transparent;
scrollbar-gutter: stable;
}
@@ -9,7 +9,7 @@
}
.ohif-scrollbar::-webkit-scrollbar {
@apply w-2;
width: 8px;
}
.ohif-scrollbar::-webkit-scrollbar-track {
@@ -19,12 +19,12 @@
.ohif-scrollbar::-webkit-scrollbar-thumb {
@apply rounded;
@apply bg-primary-main;
background-color: #3e5967;
background-color: #173239;
}
.ohif-scrollbar::-webkit-scrollbar-thumb:window-inactive {
@apply bg-primary-main;
background-color: #3e5967;
background-color: #173239;
}
/* INVISIBLE SCROLLBAR */
@@ -46,7 +46,7 @@ const variantClasses = {
outlined: {
default: '',
primary: '',
secondary: '',
secondary: 'border border-secondary-light',
white: '',
black:
'border-primary-main group-hover:bg-primary-main group-hover:border-black',
@@ -68,6 +68,12 @@ const orientationClasses = {
const baseDisplayClass = 'inline-flex';
const fullWidthDisplayClass = 'flex';
// css class that are applied for buttons that not the first or last
const nonFirstLastClasses = {
vertical: 'border-t-0 border-b-0',
horizontal: 'border-l-0 last:border-r-0',
};
const ButtonGroup = ({
children,
className,
@@ -78,6 +84,7 @@ const ButtonGroup = ({
rounded = 'medium',
size = 'medium',
variant = 'outlined',
splitBorder = true,
...other
}) => {
const ref = useRef(null);
@@ -101,13 +108,24 @@ const ButtonGroup = ({
ref={ref}
{...other}
>
{React.Children.map(children, child => {
{React.Children.map(children, (child, index) => {
const isFirst = index === 0;
const isLast = index === React.Children.count(children) - 1;
const isNotFirstOrLast = !isFirst && !isLast;
if (!React.isValidElement(child)) {
return null;
}
return React.cloneElement(child, {
className: classnames(buttonClasses, child.props.className),
className: classnames(
buttonClasses,
child.props.className,
!splitBorder &&
isNotFirstOrLast &&
nonFirstLastClasses[orientation],
!splitBorder && isLast && 'last:border-l-0'
),
disabled: child.props.disabled || disabled,
color: child.props.color || color,
fullWidth,
@@ -0,0 +1,69 @@
import React, { useState, useCallback } from 'react';
import PropTypes, { string } from 'prop-types';
import Icon from '../Icon';
import Typography from '../Typography';
/**
* REACT CheckBox component
* it has two props, checked and onChange
* checked is a boolean value
* onChange is a function that will be called when the checkbox is clicked
*
* CheckBox is a component that allows you to use as a boolean
*/
const CheckBox: React.FC<{
checked: boolean;
onChange: (state) => void;
className?: string;
label: string;
labelClassName?: string;
labelVariant?: string;
}> = ({
checked,
onChange,
label,
labelClassName,
labelVariant = 'body',
className,
}) => {
const [isChecked, setIsChecked] = useState(checked);
const handleClick = useCallback(() => {
setIsChecked(!isChecked);
onChange(!isChecked);
}, [isChecked, onChange]);
return (
<div
className={`flex items-center cursor-pointer space-x-1 ${
className ? className : ''
}`}
onClick={handleClick}
>
{isChecked ? (
<Icon name="checkbox-checked" />
) : (
<Icon name="checkbox-unchecked" />
)}
<Typography
variant={labelVariant ?? 'subtitle'}
component="p"
className={labelClassName ?? 'text-white '}
>
{label}
</Typography>
</div>
);
};
CheckBox.propTypes = {
checked: PropTypes.bool,
onChange: PropTypes.func,
label: PropTypes.string,
labelClassName: PropTypes.string,
labelVariant: PropTypes.string,
};
export default CheckBox;
@@ -0,0 +1,43 @@
import CheckBox from '../CheckBox';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: CheckBox,
title: 'Components/CheckBox',
};
<Meta title="Components/CheckBox" component={CheckBox} />
export const CheckBoxTemplate = args => <CheckBox {...args} />
<Heading title="CheckBox" componentRelativePath="CheckBox/CheckBox.tsx" />
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
CheckBox is a component that allows you to use as a boolean value
<Canvas>
<Story
name="Overview"
args={{
checked: false,
onChange: () => console.log('on change callback'),
label: "checkBoxLabel",
labelClassName: "text-black text-sm",
}}
>
{CheckBoxTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={CheckBox} />
## Contribute
<Footer componentRelativePath="CheckBox/__stories__/CheckBox.stories.mdx" />
@@ -0,0 +1,3 @@
import CheckBox from './CheckBox';
export default CheckBox;
@@ -4,12 +4,12 @@ import classnames from 'classnames';
import { Icon, Typography } from '../';
// TODO: Add loading spinner to OHIF + use it here.
const EmptyStudies = ({ className, isLoading }) => {
const EmptyStudies = ({ className }) => {
return (
<div className={classnames('flex-col inline-flex items-center', className)}>
<Icon name="magnifier" className="mb-4" />
<Typography className="text-primary-light" variant="h5">
{!isLoading ? 'No studies available' : 'Loading...'}
{'No studies available'}
</Typography>
</div>
);
@@ -38,6 +38,21 @@ import sortingActiveDown from './../../assets/icons/sorting-active-down.svg';
import sortingActiveUp from './../../assets/icons/sorting-active-up.svg';
import tracked from './../../assets/icons/tracked.svg';
import unlink from './../../assets/icons/unlink.svg';
import checkboxChecked from './../../assets/icons/checkbox-checked.svg';
import checkboxUnchecked from './../../assets/icons/checkbox-unchecked.svg';
import iconNextInactive from './../../assets/icons/icon-next-inactive.svg';
import iconNext from './../../assets/icons/icon-next.svg';
import iconPrevInactive from './../../assets/icons/icon-prev-inactive.svg';
import iconPrev from './../../assets/icons/icon-prev.svg';
import navigationPanelRightHide from './../../assets/icons/navigation-panel-right-hide.svg';
import navigationPanelRightReveal from './../../assets/icons/navigation-panel-right-reveal.svg';
import tabLinear from './../../assets/icons/tab-linear.svg';
import tabPatientInfo from './../../assets/icons/tab-patient-info.svg';
import tabROIThreshold from './../../assets/icons/tab-roi-threshold.svg';
import tabSegmentation from './../../assets/icons/tab-segmentation.svg';
import uiArrowDown from './../../assets/icons/ui-arrow-down.svg';
import uiArrowUp from './../../assets/icons/ui-arrow-up.svg';
import loadingOHIFMark from './../../assets/icons/loading-ohif-mark.svg';
/** Tools */
import toolZoom from './../../assets/icons/tool-zoom.svg';
@@ -64,6 +79,21 @@ import toolRectangle from './../../assets/icons/tool-rectangle.svg';
import toolFusionColor from './../../assets/icons/tool-fusion-color.svg';
import toolCreateThreshold from './../../assets/icons/tool-create-threshold.svg';
import editPatient from './../../assets/icons/edit-patient.svg';
import panelGroupMore from './../../assets/icons/panel-group-more.svg';
import panelGroupOpenClose from './../../assets/icons/panel-group-open-close.svg';
import rowAdd from './../../assets/icons/row-add.svg';
import rowEdit from './../../assets/icons/row-edit.svg';
import rowHidden from './../../assets/icons/row-hidden.svg';
import rowHideAll from './../../assets/icons/row-hide-all.svg';
import rowHide from './../../assets/icons/row-hide.svg';
import rowLocked from './../../assets/icons/row-locked.svg';
import rowShowAll from './../../assets/icons/row-show-all.svg';
import rowUnhide from './../../assets/icons/row-unhide.svg';
import rowUnlocked from './../../assets/icons/row-unlocked.svg';
import iconMPR from './../../assets/icons/icon-mpr-alt.svg';
import checkboxDefault from './../../assets/icons/checkbox-default.svg';
import checkboxActive from './../../assets/icons/checkbox-active.svg';
import referenceLines from './../../assets/icons/reference-lines.svg';
/** Old OHIF */
import oldTrash from './../../assets/icons/old-trash.svg';
@@ -108,6 +138,20 @@ const ICONS = {
sorting: sorting,
tracked: tracked,
unlink: unlink,
'panel-group-more': panelGroupMore,
'panel-group-open-close': panelGroupOpenClose,
'row-add': rowAdd,
'row-edit': rowEdit,
'row-hidden': rowHidden,
'row-hide-all': rowHideAll,
'row-hide': rowHide,
'row-locked': rowLocked,
'row-show-all': rowShowAll,
'row-unhide': rowUnhide,
'row-unlocked': rowUnlocked,
'checkbox-checked': checkboxChecked,
'checkbox-unchecked': checkboxUnchecked,
'loading-ohif-mark': loadingOHIFMark,
/** Tools */
'tool-zoom': toolZoom,
@@ -134,6 +178,22 @@ const ICONS = {
'tool-fusion-color': toolFusionColor,
'tool-create-threshold': toolCreateThreshold,
'edit-patient': editPatient,
'icon-mpr': iconMPR,
'icon-next-inactive': iconNextInactive,
'icon-next': iconNext,
'icon-prev-inactive': iconPrevInactive,
'icon-prev': iconPrev,
'navigation-panel-right-hide': navigationPanelRightHide,
'navigation-panel-right-reveal': navigationPanelRightReveal,
'tab-linear': tabLinear,
'tab-patient-info': tabPatientInfo,
'tab-roi-threshold': tabROIThreshold,
'tab-segmentation': tabSegmentation,
'ui-arrow-down': uiArrowDown,
'ui-arrow-up': uiArrowUp,
'checkbox-default': checkboxDefault,
'checkbox-active': checkboxActive,
'tool-referenceLines': referenceLines,
/** Old OHIF */
'old-trash': oldTrash,
@@ -8,6 +8,7 @@ class ImageScrollbar extends PureComponent {
max: PropTypes.number.isRequired,
height: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
onContextMenu: PropTypes.func,
};
render() {
@@ -20,7 +21,7 @@ class ImageScrollbar extends PureComponent {
};
return (
<div className="scroll">
<div className="scroll" onContextMenu={this.props.onContextMenu}>
<div className="scroll-holder">
<input
// adding mousetrap let the mousetrap know about the scrollbar otherwise,
@@ -66,4 +67,8 @@ class ImageScrollbar extends PureComponent {
};
}
ImageScrollbar.defaultProps = {
onContextMenu: e => e.preventDefault(),
};
export default ImageScrollbar;
@@ -0,0 +1,17 @@
input[type='text'] {
-webkit-appearance: none;
}
input[type='text']:focus {
border: none;
outline: none;
}
.up-arrowsize svg {
width: 7px;
height: 7px;
}
.up-arrowsize svg path {
fill: #726f7e;
}
@@ -0,0 +1,96 @@
import React, { useState, useCallback } from 'react';
import IconButton from '../IconButton';
import Icon from '../Icon';
import './InputNumber.css';
/**
* React Number Input component'
* it has two props, value and onChange
* value is a number value
* onChange is a function that will be called when the number input is changed
* it can get changed by clicking on the up and down buttons
* or by typing a number in the input
*/
const sizesClasses = {
sm: 'w-[45px] h-[28px]',
};
const InputNumber: React.FC<{
value: number;
onChange: (value) => void;
step: number;
size?: string;
className?: string;
}> = ({ value, onChange, step = 1, className, size = 'sm' }) => {
const [numberValue, setNumberValue] = useState(value);
const handleChange = useCallback(
e => {
const numberValue = e.target.value;
setNumberValue(numberValue);
onChange(numberValue);
},
[onChange, setNumberValue]
);
const handleIncrement = useCallback(
e => {
const newNum = Number(numberValue) + step;
setNumberValue(newNum);
onChange(newNum);
},
[onChange, setNumberValue, step, numberValue]
);
const handleDecrement = useCallback(
e => {
const newNum = Number(numberValue) - step;
setNumberValue(newNum);
onChange(newNum);
},
[onChange, setNumberValue, step, numberValue]
);
return (
<div
className={`flex items-center bg-black border-2 px-1 overflow-hidden justify-center border-secondary-light rounded-md ${
sizesClasses[size]
} ${className ? className : ''}`}
>
<div className="flex">
<input
type="text"
value={numberValue}
onChange={handleChange}
className={`bg-black text-white text-[12px] w-full text-center`}
/>
<div className="up-arrowsize flex flex-col items-center justify-around">
<IconButton
id={'down-arrow-icon'}
variant="text"
color="inherit"
size="initial"
className="text-[#726f7e] transform rotate-180"
onClick={handleIncrement}
>
<Icon name="ui-arrow-down" />
</IconButton>
<IconButton
id={'down-arrow-icon'}
variant="text"
color="inherit"
size="initial"
className="text-[#726f7e]"
onClick={handleDecrement}
>
<Icon name="ui-arrow-down" />
</IconButton>
</div>
</div>
</div>
);
};
export default InputNumber;
@@ -0,0 +1,47 @@
import InputNumber from '../InputNumber';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: InputNumber,
title: 'Components/InputNumber',
};
<Meta title="Components/InputNumber" component={InputNumber} />
export const InputNumberTemplate = args => (
<div className="w-16">
<InputNumber {...args} />
</div>
)
<Heading title="InputNumber" componentRelativePath="InputNumber/InputNumber.tsx" />
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
InputNumber is a component that allows you to use as a number value
<Canvas>
<Story
name="Overview"
args={{
step: 1,
value: 3,
size: 'sm',
onChange: () => console.log('input range change'),
}}
>
{InputNumberTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={InputNumber} />
## Contribute
<Footer componentRelativePath="InputNumber/__stories__/InputNumber.stories.mdx" />
@@ -0,0 +1,3 @@
import InputNumber from './InputNumber';
export default InputNumber;
@@ -0,0 +1,26 @@
input[type='range'] {
-webkit-appearance: none;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
border: none;
height: 10px;
width: 10px;
border-radius: 50%;
background: #5acce6;
}
input[type='range']::-moz-range-thumb {
-webkit-appearance: none;
border: none;
height: 10px;
width: 10px;
border-radius: 50%;
background: #5acce6;
}
.input-range-thumb-design[type='range']::-webkit-slider-thumb {
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.521);
border: 1.5px solid #090c29;
}
@@ -0,0 +1,86 @@
import React, { useState, useCallback } from 'react';
import classNames from 'classnames';
import Typography from '../Typography';
import './InputRange.css';
/**
* React Range Input component
* it has two props, value and onChange
* value is a number value
* onChange is a function that will be called when the range input is changed
*
*
*/
const InputRange: React.FC<{
value: number;
onChange: (value) => void;
minValue: number;
maxValue: number;
step: number;
unit?: string;
containerClassName?: string;
inputClassName?: string;
labelClassName?: string;
labelVariant?: string;
}> = ({
value,
onChange,
minValue,
maxValue,
step = 1,
unit = '',
containerClassName,
inputClassName,
labelClassName,
labelVariant,
}) => {
const [rangeValue, setRangeValue] = useState(value);
const handleChange = useCallback(
e => {
const rangeValue = e.target.value;
setRangeValue(rangeValue);
onChange(rangeValue);
},
[onChange, setRangeValue]
);
const rangeValuePercentage =
((rangeValue - minValue) / (maxValue - minValue)) * 100;
return (
<div
className={`flex items-center cursor-pointer space-x-1 ${
containerClassName ? containerClassName : ''
}`}
>
<input
type="range"
min={minValue}
max={maxValue}
value={rangeValue}
className={`appearance-none h-[3px] rounded-lg input-range-thumb-design ${
inputClassName ? inputClassName : ''
}`}
style={{
background: `linear-gradient(to right, #5acce6 0%, #5acce6 ${rangeValuePercentage -
10}%, #3a3f99 ${rangeValuePercentage + 10}%, #3a3f99 100%)`,
}}
onChange={handleChange}
id="myRange"
step={step}
/>
<Typography
variant={labelVariant ?? 'subtitle'}
component="p"
className={classNames('w-8', labelClassName ?? 'text-white')}
>
{rangeValue}
{unit}
</Typography>
</div>
);
};
export default InputRange;
@@ -0,0 +1,50 @@
import InputRange from '../InputRange';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
export const argTypes = {
component: InputRange,
title: 'Components/InputRange',
};
<Meta title="Components/InputRange" component={InputRange} />
export const RangeInputTemplate = args => (
<div className="w-3">
<InputRange {...args} />
</div>
)
<Heading title="InputRange" componentRelativePath="InputRange/InputRange.tsx" />
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
InputRange is a component that allows you to use as a boolean value
<Canvas>
<Story
name="Overview"
args={{
value: 10,
minValue: 0,
maxValue: 300,
step: 10,
unit: '%',
onChange: () => console.log('input range change'),
labelClassName: 'text-black'
}}
>
{RangeInputTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={InputRange} />
## Contribute
<Footer componentRelativePath="InputRange/__stories__/InputRange.stories.mdx" />
@@ -0,0 +1,3 @@
import InputRange from './InputRange';
export default InputRange;
@@ -0,0 +1,793 @@
import React from 'react';
import Lottie from 'lottie-react';
import classNames from 'classnames';
const LoadingAnimation = {
v: '5.9.6',
fr: 60,
ip: 0,
op: 106,
w: 215,
h: 215,
nm: 'ohif-progress',
ddd: 0,
assets: [],
layers: [
{
ddd: 0,
ind: 1,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.797] },
o: { x: [0.274], y: [0.135] },
t: 25,
s: [0],
},
{
i: { x: [0.828], y: [0.97] },
o: { x: [0.446], y: [0.039] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.667, y: 0.667 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [130.5, 131.75, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.806 },
o: { x: 0.167, y: 0.006 },
t: 9,
s: [130.5, 131.75, 0],
to: [2.917, 2.875, 0],
ti: [-2.917, -2.875, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [148, 149, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.811 },
o: { x: 0.167, y: 0.17 },
t: 52,
s: [148, 149, 0],
to: [-0.881, -0.866, 0],
ti: [1.995, 1.961, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0 },
t: 76,
s: [108.4, 113.023, 0],
to: [-1.196, -1.176, 0],
ti: [-2.169, -1.843, 0],
},
{
i: { x: 0.833, y: 0.832 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [108.4, 113, 0],
to: [1.272, 1.081, 0],
ti: [-1.819, -1.575, 0],
},
{
i: { x: 0.833, y: 0.941 },
o: { x: 0.167, y: 0.008 },
t: 95,
s: [115.214, 118.607, 0],
to: [7.038, 6.091, 0],
ti: [1.198, 1.177, 0],
},
{ t: 106, s: [130.5, 131.8, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
{
ddd: 0,
ind: 3,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.79] },
o: { x: [0.278], y: [0.149] },
t: 25,
s: [0],
},
{
i: { x: [0.828], y: [1.005] },
o: { x: [0.446], y: [0.022] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [81, 132, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.808 },
o: { x: 0.167, y: 0.011 },
t: 9,
s: [81, 132, 0],
to: [-3, 3, 0],
ti: [3, -3, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [63, 150, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.812 },
o: { x: 0.167, y: 0.174 },
t: 52,
s: [63, 150, 0],
to: [0.906, -0.906, 0],
ti: [-2.052, 2.052, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.002 },
t: 76,
s: [108.685, 113.315, 0],
to: [1.217, -1.217, 0],
ti: [2.674, -1.792, 0],
},
{
i: { x: 0.833, y: 0.834 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [108.7, 113.3, 0],
to: [-1.593, 1.067, 0],
ti: [2.246, -1.581, 0],
},
{
i: { x: 0.833, y: 0.942 },
o: { x: 0.167, y: 0.014 },
t: 95,
s: [99.965, 118.742, 0],
to: [-8.674, 6.104, 0],
ti: [-1.236, 1.236, 0],
},
{ t: 106, s: [81, 132, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
{
ddd: 0,
ind: 5,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.79] },
o: { x: [0.278], y: [0.149] },
t: 25,
s: [0],
},
{
i: { x: [0.828], y: [0.943] },
o: { x: [0.446], y: [0.022] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [131, 84.75, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.808 },
o: { x: 0.167, y: 0.014 },
t: 9,
s: [131, 84.75, 0],
to: [3, -3.125, 0],
ti: [-3, 3.125, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [149, 66, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.812 },
o: { x: 0.167, y: 0.175 },
t: 52,
s: [149, 66, 0],
to: [-0.906, 0.946, 0],
ti: [2.052, -2.143, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.013 },
t: 76,
s: [109.315, 112.893, 0],
to: [-1.222, 1.276, 0],
ti: [-2.102, 2.732, 0],
},
{
i: { x: 0.833, y: 0.835 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [109.3, 112.9, 0],
to: [1.242, -1.615, 0],
ti: [-1.796, 2.277, 0],
},
{
i: { x: 0.833, y: 0.943 },
o: { x: 0.167, y: 0.016 },
t: 95,
s: [115.852, 104.071, 0],
to: [6.96, -8.823, 0],
ti: [1.236, -1.29, 0],
},
{ t: 106, s: [131, 84.8, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
{
ddd: 0,
ind: 7,
ty: 4,
nm: 'Layer 1/ohif-shape-new Outlines',
sr: 1,
ks: {
o: { a: 0, k: 100, ix: 11 },
r: {
a: 1,
k: [
{
i: { x: [0.833], y: [0.79] },
o: { x: [0.278], y: [0.149] },
t: 25,
s: [0],
},
{
i: { x: [0.823], y: [1] },
o: { x: [0.446], y: [-0.058] },
t: 52,
s: [90],
},
{
i: { x: [0.83], y: [1.207] },
o: { x: [0.167], y: [-0.246] },
t: 65,
s: [90],
},
{ t: 85, s: [180] },
],
ix: 10,
},
p: {
a: 1,
k: [
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.167 },
t: 0,
s: [80.5, 85, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.833, y: 0.806 },
o: { x: 0.167, y: 0.005 },
t: 9,
s: [80.5, 85, 0],
to: [-2.75, -3, 0],
ti: [2.75, 3, 0],
},
{
i: { x: 0.872, y: 0.872 },
o: { x: 0.167, y: 0.167 },
t: 40,
s: [64, 67, 0],
to: [0, 0, 0],
ti: [0, 0, 0],
},
{
i: { x: 0.856, y: 0.812 },
o: { x: 0.167, y: 0.177 },
t: 52,
s: [64, 67, 0],
to: [0.831, 0.906, 0],
ti: [-1.881, -2.052, 0],
},
{
i: { x: 0.833, y: 0.833 },
o: { x: 0.167, y: 0.025 },
t: 76,
s: [109.295, 112.685, 0],
to: [1.112, 1.213, 0],
ti: [2.768, 2.657, 0],
},
{
i: { x: 0.833, y: 0.837 },
o: { x: 0.167, y: 0.167 },
t: 85,
s: [109.3, 112.7, 0],
to: [-1.648, -1.581, 0],
ti: [2.299, 2.234, 0],
},
{
i: { x: 0.833, y: 0.943 },
o: { x: 0.167, y: 0.019 },
t: 95,
s: [100.116, 104.028, 0],
to: [-8.935, -8.681, 0],
ti: [-1.136, -1.239, 0],
},
{ t: 106, s: [80.5, 85, 0] },
],
ix: 2,
l: 2,
},
a: { a: 0, k: [21, 21, 0], ix: 1, l: 2 },
s: { a: 0, k: [100, 100, 100], ix: 6, l: 2 },
},
ao: 0,
shapes: [
{
ty: 'gr',
it: [
{
ind: 0,
ty: 'sh',
ix: 1,
ks: {
a: 0,
k: {
i: [
[-1.657, 0],
[0, 0],
[0, -1.657],
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
],
o: [
[0, 0],
[1.657, 0],
[0, 0],
[0, 1.657],
[0, 0],
[-1.657, 0],
[0, 0],
[0, -1.657],
],
v: [
[-13, -16],
[13, -16],
[16, -13],
[16, 13],
[13, 16],
[-13, 16],
[-16, 13],
[-16, -13],
],
c: true,
},
ix: 2,
},
nm: 'Path 1',
mn: 'ADBE Vector Shape - Group',
hd: false,
},
{
ty: 'st',
c: { a: 0, k: [1, 1, 1, 1], ix: 3 },
o: { a: 0, k: 100, ix: 4 },
w: { a: 0, k: 2, ix: 5 },
lc: 1,
lj: 1,
ml: 10,
bm: 0,
nm: 'Stroke 1',
mn: 'ADBE Vector Graphic - Stroke',
hd: false,
},
{
ty: 'tr',
p: { a: 0, k: [21, 21], ix: 2 },
a: { a: 0, k: [0, 0], ix: 1 },
s: { a: 0, k: [100, 100], ix: 3 },
r: { a: 0, k: 0, ix: 6 },
o: { a: 0, k: 100, ix: 7 },
sk: { a: 0, k: 0, ix: 4 },
sa: { a: 0, k: 0, ix: 5 },
nm: 'Transform',
},
],
nm: 'Group 1',
np: 2,
cix: 2,
bm: 0,
ix: 1,
mn: 'ADBE Vector Group',
hd: false,
},
],
ip: 0,
op: 106,
st: 0,
ct: 1,
bm: 0,
},
],
markers: [],
};
const LoadingIndicator = ({ className }) => {
return (
<div
className={classNames(
'absolute z-50 top-0 left-0 flex flex-col items-center justify-center',
className
)}
>
<Lottie animationData={LoadingAnimation} />;
</div>
);
};
export default LoadingIndicator;
@@ -0,0 +1,2 @@
import LoadingIndicator from './LoadingIndicator';
export default LoadingIndicator;
@@ -0,0 +1,34 @@
.loading {
background-color: #091731;
height: 8px;
border-radius: 4px;
overflow: hidden;
position: relative;
width: 12em;
}
.infinite-loading-bar {
animation: side2side 2s ease-in-out infinite;
height: 100%;
position: absolute;
border-radius: 4px;
width: 50%;
}
.loading-bar {
animation: side2side 2s ease-in-out infinite;
height: 100%;
border-radius: 4px;
position: absolute;
width: 50%;
}
@keyframes side2side {
0%,
100% {
transform: translateX(-50%);
}
50% {
transform: translateX(150%);
}
}
@@ -0,0 +1,40 @@
import React from 'react';
import classNames from 'classnames';
import { Icon } from '@ohif/ui';
import './LoadingIndicatorProgress.css';
/**
* A React component that renders a loading indicator.
* if progress is not provided, it will render an infinite loading indicator
* if progress is provided, it will render a progress bar
* Optionally a textBlock can be provided to display a message
*/
function LoadingIndicatorProgress({ className, textBlock, progress }) {
return (
<div
className={classNames(
'absolute z-50 top-0 left-0 flex flex-col items-center justify-center space-y-5',
className
)}
>
<Icon name="loading-ohif-mark" className="text-white w-12 h-12" />
<div className="loading">
{progress === undefined || progress === null ? (
<div className="infinite-loading-bar bg-primary-light"></div>
) : (
<div
className="bg-primary-light"
style={{
width: `${progress}%`,
height: '8px',
}}
></div>
)}
</div>
{textBlock}
</div>
);
}
export default LoadingIndicatorProgress;
@@ -0,0 +1,2 @@
import LoadingIndicatorProgress from './LoadingIndicatorProgress';
export default LoadingIndicatorProgress;
@@ -4,8 +4,9 @@ import { useTranslation } from 'react-i18next';
import MeasurementItem from './MeasurementItem';
const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
const MeasurementTable = ({ data, title, onClick, onEdit }) => {
const { t } = useTranslation('MeasurementTable');
const amount = data.length;
return (
<div>
@@ -45,7 +46,6 @@ const MeasurementTable = ({ data, title, amount, onClick, onEdit }) => {
};
MeasurementTable.defaultProps = {
amount: null,
data: [],
onClick: () => {},
onEdit: () => {},
@@ -53,7 +53,6 @@ MeasurementTable.defaultProps = {
MeasurementTable.propTypes = {
title: PropTypes.string.isRequired,
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
data: PropTypes.arrayOf(
PropTypes.shape({
uid: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+1 -1
View File
@@ -9,7 +9,7 @@ const NavBar = ({ className, children, isSticky }) => {
return (
<div
className={classnames(
'flex flex-row items-center bg-secondary-dark px-3 border-b-4 border-black z-20',
'flex flex-row items-center bg-secondary-dark px-1 border-b-4 border-black z-20',
isSticky && stickyClasses,
!isSticky && notStickyClasses,
className
@@ -0,0 +1,149 @@
import React from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import { Icon, Tooltip } from '../';
import { useTranslation } from 'react-i18next';
const classes = {
infoHeader: 'text-base text-primary-light',
infoText: 'text-base text-white max-w-24 truncate',
firstRow: 'flex flex-col',
row: 'flex flex-col ml-4',
};
function PatientInfo({
patientName,
patientSex,
patientAge,
MRN,
thickness,
spacing,
scanner,
isOpen,
showPatientInfoRef,
}) {
const { t } = useTranslation('PatientInfo');
while (patientAge.charAt(0) === '0') {
patientAge = patientAge.substr(1);
}
return (
<div ref={showPatientInfoRef}>
<Tooltip
isSticky
isDisabled={!isOpen}
position="bottom-right"
content={
isOpen && (
<div className="flex py-2">
<div className="flex pt-1">
<Icon name="info-link" className="w-4 text-primary-main" />
</div>
<div className="flex flex-col ml-2">
<span
className="text-base font-bold text-white"
title={patientName}
>
{patientName}
</span>
<div className="flex pb-4 mt-4 mb-4 border-b border-secondary-main">
<div className={classnames(classes.firstRow)}>
<span className={classnames(classes.infoHeader)}>
{t('Sex')}
</span>
<span
className={classnames(classes.infoText)}
title={patientSex}
>
{patientSex}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('Age')}
</span>
<span
className={classnames(classes.infoText)}
title={patientAge}
>
{patientAge}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('MRN')}
</span>
<span className={classnames(classes.infoText)} title={MRN}>
{MRN}
</span>
</div>
</div>
<div className="flex">
<div className={classnames(classes.firstRow)}>
<span className={classnames(classes.infoHeader)}>
{t('Thickness')}
</span>
<span
className={classnames(classes.infoText)}
title={thickness}
>
{thickness}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('Spacing')}
</span>
<span
className={classnames(classes.infoText)}
title={spacing}
>
{spacing}
</span>
</div>
<div className={classnames(classes.row)}>
<span className={classnames(classes.infoHeader)}>
{t('Scanner')}
</span>
<span
className={classnames(classes.infoText)}
title={scanner}
>
{scanner}
</span>
</div>
</div>
</div>
</div>
)
}
>
<div className="relative flex justify-end cursor-pointer">
<div className="relative">
<Icon name="profile" className="w-5 text-white" />
<Icon
name="info-link"
className="absolute w-5 text-white bg-black"
style={{ right: -7, bottom: -10 }}
/>
</div>
</div>
</Tooltip>
</div>
);
}
PatientInfo.propTypes = {
patientName: PropTypes.string,
patientSex: PropTypes.string,
patientAge: PropTypes.string,
MRN: PropTypes.string,
thickness: PropTypes.string,
spacing: PropTypes.string,
scanner: PropTypes.string,
isOpen: PropTypes.bool,
showPatientInfoRef: PropTypes.object,
};
export default PatientInfo;
@@ -0,0 +1,2 @@
import PatientInfo from './PatientInfo';
export default PatientInfo;
@@ -0,0 +1,256 @@
import React, { useState } from 'react';
import { Icon, InputRange, CheckBox, InputNumber } from '../';
import classNames from 'classnames';
import { reducer } from './segmentationConfigReducer';
const ActiveSegmentationConfig = ({
config,
dispatch,
setRenderOutline,
setOutlineOpacityActive,
setOutlineWidthActive,
setRenderFill,
setFillAlpha,
usePercentage,
}) => {
return (
<div className="flex justify-between text-[12px] pt-[13px] px-2">
<div className="flex flex-col items-start">
<div className="text-white mb-[12px]">Active</div>
<CheckBox
label="Outline"
checked={config.renderOutline}
labelClassName="text-[12px] pl-1 pt-1"
className="mb-[9px]"
onChange={value => {
dispatch({
type: 'RENDER_OUTLINE',
payload: {
value,
},
});
setRenderOutline(value);
}}
/>
<CheckBox
label="Fill"
checked={config.renderFill}
labelClassName="text-[12px] pl-1 pt-1"
className="mb-[9px]"
onChange={value => {
dispatch({
type: 'RENDER_FILL',
payload: {
value,
},
});
setRenderFill(value);
}}
/>
</div>
<div className="flex flex-col items-center col-span-2">
<div className="text-[#b3b3b3] text-[10px] mb-[12px]">Opacity</div>
<InputRange
minValue={0}
maxValue={usePercentage ? 100 : 1}
value={
usePercentage ? config.outlineOpacity * 100 : config.outlineOpacity
}
onChange={value => {
dispatch({
type: 'SET_OUTLINE_OPACITY',
payload: {
value: value,
},
});
setOutlineOpacityActive(value);
}}
step={1}
containerClassName="mt-[4px] mb-[9px]"
inputClassName="w-[64px]"
labelClassName="text-white text-[12px]"
unit="%"
/>
<InputRange
minValue={0}
maxValue={usePercentage ? 100 : 1}
value={usePercentage ? config.fillAlpha * 100 : config.fillAlpha}
onChange={value => {
dispatch({
type: 'SET_FILL_ALPHA',
payload: {
value,
},
});
setFillAlpha(value);
}}
step={1}
containerClassName="mt-[4px] mb-[9px]"
inputClassName="w-[64px]"
labelClassName="text-white text-[12px]"
unit="%"
/>
</div>
<div className="flex flex-col items-center">
<div className="text-[#b3b3b3] text-[10px] mb-[12px]">Size</div>
<InputNumber
value={config.outlineWidthActive}
onChange={value => {
dispatch({
type: 'SET_OUTLINE_WIDTH',
payload: {
value,
},
});
setOutlineWidthActive(value);
}}
className="-mt-1"
/>
</div>
</div>
);
};
const InactiveSegmentationConfig = ({
config,
dispatch,
setRenderInactiveSegmentations,
setFillAlphaInactive,
usePercentage,
}) => {
return (
<div className="px-2">
<CheckBox
label="Display Inactive Segmentations"
checked={config.renderInactiveSegmentations}
labelClassName="text-[12px] pt-1"
className="mb-[9px]"
onChange={value => {
dispatch({
type: 'RENDER_INACTIVE_SEGMENTATIONS',
payload: {
value,
},
});
setRenderInactiveSegmentations(value);
}}
/>
<div className="flex pl-4 items-center space-x-2">
<span className="text-[10px] text-[#b3b3b3]">Opacity</span>
<InputRange
minValue={0}
maxValue={usePercentage ? 100 : 1}
value={
usePercentage
? config.fillAlphaInactive * 100
: config.fillAlphaInactive
}
onChange={value => {
dispatch({
type: 'SET_FILL_ALPHA_INACTIVE',
payload: {
value: value,
},
});
setFillAlphaInactive(value);
}}
step={1}
containerClassName="mt-[4px]"
inputClassName="w-[64px]"
labelClassName="text-white text-[12px]"
unit="%"
/>
</div>
</div>
);
};
const SegmentationConfig = ({
segmentationConfig,
setFillAlpha,
setFillAlphaInactive,
setOutlineWidthActive,
setOutlineOpacityActive,
setRenderFill,
setRenderInactiveSegmentations,
setRenderOutline,
}) => {
const [config, dispatch] = React.useReducer(
reducer,
segmentationConfig.initialConfig
);
const [isMinimized, setIsMinimized] = useState(true);
return (
<div className="bg-primary-dark">
<div
className="flex cursor-pointer items-center"
onClick={evt => {
evt.stopPropagation();
setIsMinimized(!isMinimized);
}}
>
<Icon
name="panel-group-open-close"
className={classNames(
'w-5 h-5 text-white transition duration-300 cursor-pointer',
{
'transform rotate-90': !isMinimized,
}
)}
/>
<span className="text-[#d8d8d8] text-[12px] font-[300]">
{'Segmentation Appearance'}
</span>
</div>
{/* active segmentation */}
{!isMinimized && (
<div>
<ActiveSegmentationConfig
config={config}
dispatch={dispatch}
setFillAlpha={setFillAlpha}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderOutline={setRenderOutline}
usePercentage={segmentationConfig.usePercentage}
/>
{/* A small line */}
<div className="h-[1px] bg-[#212456] mb-[8px] mx-1"></div>
{/* inactive segmentation */}
<div
onClick={() => setIsMinimized(!isMinimized)}
className="flex items-center cursor-pointer pl-2 pb-[9px]"
>
<span className="text-[#d8d8d8] text-[12px] font-[300]">
{'Inactive Segmentations'}
</span>
</div>
<InactiveSegmentationConfig
config={config}
dispatch={dispatch}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setFillAlphaInactive={setFillAlphaInactive}
usePercentage={segmentationConfig.usePercentage}
/>
</div>
)}
<div className="h-[6px] bg-black "></div>
</div>
);
};
SegmentationConfig.propTypes = {};
export default SegmentationConfig;
@@ -0,0 +1,273 @@
import React from 'react';
import PropTypes from 'prop-types';
import SegmentationGroupSegment from './SegmentationGroupSegment';
import Dropdown from '../Dropdown';
import classnames from 'classnames';
import Icon from '../Icon';
import IconButton from '../IconButton';
const AddNewSegmentRow = ({
id,
onConfigChange,
onToggleSegmentationVisibility,
onSegmentAdd,
isVisible,
showAddSegment,
}) => {
return (
<div>
<div className="flex items-center pl-[29px] bg-black text-primary-active hover:opacity-80 cursor-pointer text-[12px] py-1">
{showAddSegment && (
<div className="flex items-center" onClick={() => onSegmentAdd()}>
<Icon name="row-add" className="w-5 h-5" />
<div className="">Add Segment</div>
</div>
)}
<div className="flex-grow" />
<div
className="flex items-center pr-2"
onClick={() => {
onToggleSegmentationVisibility(id);
}}
>
{isVisible ? (
<Icon name="row-show-all" className="w-5 h-5" />
) : (
<Icon name="row-hide-all" className="w-5 h-5" />
)}
</div>
</div>
{/* {isSegmentationConfigOpen && (
<SegmentationConfig onConfigChange={onConfigChange} />
)} */}
</div>
);
};
const SegmentGroupHeader = ({
isMinimized,
onToggleMinimizeSegmentation,
onSegmentationClick,
id,
label,
isActive,
segmentCount,
onSegmentationEdit,
onSegmentationDelete,
}) => {
return (
<div
className={classnames(
'flex items-center pr-2 pl-[3px] h-[27px] gap-2 rounded-t-md border-b border-secondary-light cursor-pointer text-[12px]',
{
'bg-secondary-main': isActive,
'bg-secondary-dark': !isActive,
}
)}
onClick={evt => {
evt.stopPropagation();
onSegmentationClick(id);
}}
>
<Icon
name="panel-group-open-close"
onClick={evt => {
evt.stopPropagation();
onToggleMinimizeSegmentation(id);
}}
className={classnames(
'w-5 h-5 text-white transition duration-300 cursor-pointer',
{
'transform rotate-90': !isMinimized,
}
)}
/>
<span className="text-white ">{label.toUpperCase()}</span>
<div className="flex-grow" />
<span className="text-white ">{segmentCount}</span>
<div
onClick={e => {
e.stopPropagation();
}}
>
<Dropdown
id="segmentation-dropdown"
showDropdownIcon={false}
list={[
{
title: 'Rename',
onClick: () => {
onSegmentationEdit(id);
},
},
{
title: 'Delete',
onClick: () => {
onSegmentationDelete(id);
},
},
]}
>
<IconButton
id={''}
variant="text"
color="inherit"
size="initial"
className="text-primary-active"
>
<Icon name="panel-group-more" />
</IconButton>
</Dropdown>
</div>
</div>
);
};
const SegmentationGroup = ({
label,
id,
segmentCount,
isVisible,
isActive,
onSegmentClick,
isMinimized,
onSegmentColorClick,
showAddSegment,
segments,
activeSegmentIndex,
onSegmentAdd,
onSegmentationClick,
onClickSegmentColor,
onSegmentationEdit,
onToggleSegmentVisibility,
onToggleSegmentationVisibility,
onSegmentDelete,
showSegmentDelete,
onToggleMinimizeSegmentation,
onSegmentationConfigChange,
onSegmentationDelete,
onSegmentEdit,
}) => {
return (
<div className="flex flex-col">
<SegmentGroupHeader
id={id}
label={label}
isActive={isActive}
isMinimized={isMinimized}
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
onSegmentationClick={onSegmentationClick}
segmentCount={segmentCount}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDelete={onSegmentationDelete}
/>
{!isMinimized && (
<div className="">
<AddNewSegmentRow
onConfigChange={onSegmentationConfigChange}
onSegmentAdd={onSegmentAdd}
isVisible={isVisible}
onToggleSegmentationVisibility={onToggleSegmentationVisibility}
id={id}
showAddSegment={showAddSegment}
/>
<div
className="ohif-scrollbar overflow-y-hidden"
style={{ maxHeight: '40rem' }}
>
{!!segments.length &&
segments.map(segment => {
if (segment === undefined || segment === null) {
return null;
}
const {
segmentIndex,
color,
label,
isVisible,
isLocked,
} = segment;
return (
<div className="mb-[1px]" key={segmentIndex}>
<SegmentationGroupSegment
segmentationId={id}
segmentIndex={segmentIndex}
label={label}
color={color}
isActive={activeSegmentIndex === segmentIndex}
isLocked={isLocked}
isVisible={isVisible}
onClick={onSegmentClick}
onEdit={onSegmentEdit}
onDelete={onSegmentDelete}
showSegmentDelete={showSegmentDelete}
onColor={onSegmentColorClick}
onToggleVisibility={onToggleSegmentVisibility}
/>
</div>
);
})}
</div>
</div>
)}
</div>
);
};
SegmentationGroup.propTypes = {
label: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
segments: PropTypes.array.isRequired,
segmentCount: PropTypes.number.isRequired,
isVisible: PropTypes.bool.isRequired,
isActive: PropTypes.bool.isRequired,
isMinimized: PropTypes.bool.isRequired,
activeSegmentIndex: PropTypes.number,
onClickNewSegment: PropTypes.func.isRequired,
onClickSegment: PropTypes.func.isRequired,
onClickSegmentEdit: PropTypes.func.isRequired,
onClickSegmentDelete: PropTypes.func.isRequired,
onToggleSegmentLocked: PropTypes.func,
onToggleSegmentVisibility: PropTypes.func.isRequired,
onToggleSegmentationVisibility: PropTypes.func.isRequired,
onSegmentClick: PropTypes.func.isRequired,
showAddSegment: PropTypes.bool.isRequired,
onSegmentAdd: PropTypes.func.isRequired,
onSegmentationClick: PropTypes.func.isRequired,
onClickSegmentColor: PropTypes.func.isRequired,
onSegmentationEdit: PropTypes.func.isRequired,
onSegmentDelete: PropTypes.func.isRequired,
onToggleMinimizeSegmentation: PropTypes.func.isRequired,
onSegmentationConfigChange: PropTypes.func.isRequired,
onSegmentationDelete: PropTypes.func.isRequired,
onSegmentEdit: PropTypes.func.isRequired,
};
SegmentationGroup.defaultProps = {
label: '',
segmentCount: 0,
segments: [],
isVisible: true,
isMinimized: false,
onClickNewSegment: () => {},
onClickSegment: () => {},
onClickSegmentEdit: () => {},
onClickSegmentDelete: () => {},
onToggleSegmentLocked: () => {},
onToggleSegmentVisibility: () => {},
onToggleSegmentationVisibility: () => {},
onSegmentClick: () => {},
showAddSegment: false,
onSegmentAdd: () => {},
onSegmentationClick: () => {},
onClickSegmentColor: () => {},
onSegmentationEdit: () => {},
onSegmentDelete: () => {},
onToggleMinimizeSegmentation: () => {},
onSegmentationConfigChange: () => {},
onSegmentationDelete: () => {},
onSegmentEdit: () => {},
};
export default SegmentationGroup;
@@ -0,0 +1,242 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Icon } from '@ohif/ui';
const SegmentItem = ({
segmentIndex,
segmentationId,
label,
isActive,
isVisible,
color,
showSegmentDelete,
isLocked = false,
onClick,
onEdit,
onDelete,
onColor,
onToggleVisibility,
onToggleLocked,
}) => {
const [isHovering, setIsHovering] = useState(false);
const [isSegmentIndexHovering, setIsSegmentIndexHovering] = useState(false);
const onMouseEnter = () => setIsHovering(true);
const onMouseLeave = () => setIsHovering(false);
const cssColor = `rgb(${color[0]},${color[1]},${color[2]})`;
return (
<div
className={classnames(
'group relative flex cursor-pointer bg-primary-dark transition duration-300 text-[12px] overflow-hidden border',
{
'border-primary-light rounded-[3px]': isActive,
},
{
'border-transparent': !isActive,
}
)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={e => {
e.stopPropagation();
onClick(segmentationId, segmentIndex);
}}
role="button"
tabIndex={0}
data-cy={'segment-item'}
>
<div
className={classnames(
'w-[27px] flex items-center justify-center border-r border-r-black text-[12px]',
{
'bg-primary-light text-black border-l border-l-primary-light rounded-l-sm': isActive,
'bg-[#1d3c58] border-l border-l-[#1d3c58] rounded-l-sm':
!isActive && isHovering,
'bg-primary-dark text-aqua-pale':
!isActive && !isHovering && isVisible,
'bg-[#140e2e] opacity-60 text-[#537594]': !isActive && !isVisible,
}
)}
onMouseEnter={() => setIsSegmentIndexHovering(true)}
onMouseLeave={() => setIsSegmentIndexHovering(false)}
>
{isSegmentIndexHovering && showSegmentDelete ? (
<Icon
name="close"
className={classnames('w-5 h-5 pr-2')}
onClick={e => {
e.stopPropagation();
onDelete(segmentationId, segmentIndex);
}}
/>
) : (
<div className={classnames('flex items-center pr-2 ')}>
{segmentIndex}
</div>
)}
</div>
<div
className={classnames(
'flex items-center justify-between w-full pl-2 py-1 text-white border-r border-r-black relative ',
{
'bg-primary-dark text-primary-light': isActive,
'bg-primary-dark text-aqua-pale': !isActive && isVisible,
'bg-[#140e2e] opacity-60 text-[#3d5871]': !isVisible,
}
)}
>
<div className={classnames('flex items-baseline gap-2')}>
<div
className={classnames('shrink-0 w-[8px] h-[8px] rounded-full')}
style={{ backgroundColor: cssColor }}
onClick={e => {
e.stopPropagation();
onColor(segmentationId, segmentIndex);
}}
/>
<div className="">{label}</div>
</div>
{/* with faded background */}
<div className="absolute right-0 bg-black/15 rounded-lg pr-[7px]">
{!isVisible && !isHovering && (
<div>
<Icon
name="row-hidden"
className={classnames('w-5 h-5 text-[#3d5871] ')}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
</div>
)}
{isHovering && (
<div className={classnames('flex items-center')}>
<Icon
name="row-edit"
className={classnames('w-5 h-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onEdit(segmentationId, segmentIndex);
}}
/>
{isVisible ? (
<Icon
name="row-hide"
className={classnames('w-5 h-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
) : (
<Icon
name="row-unhide"
className={classnames('w-5 h-5', {
'text-white': isLocked,
'text-primary-light': !isLocked,
})}
onClick={e => {
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
)}
</div>
)}
</div>
</div>
{/* <div className="relative flex flex-col w-full border-t border-t-black">
<div className="fl`ex items-center mb-1 ml-2">
<div className="flex items-center flex-1 text-base text-primary-light">
<div
className={classnames(
'w-3 h-3 mt-1 mr-2 rounded-full cursor-pointer transition duration-300 hover:opacity-80'
)}
onClick={e => {
e.stopPropagation();
onColor(segmentationId, segmentIndex);
}}
style={{ backgroundColor: cssColor }}
></div>
{label}
</div>
<div className="flex items-center w-1/3">
<div className="px-1">
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={isVisible ? 'eye-visible' : 'eye-hidden'}
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onToggleVisibility(segmentationId, segmentIndex);
}}
/>
</div>
{onToggleLocked !== undefined ? (
<div className="px-1">
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={isLocked ? 'lock' : 'dotted-circle'}
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onToggleLocked(segmentationId, segmentIndex);
}}
/>
</div>
) : null}
<div className="px-1">
<Icon
className={classnames(
'text-white w-4 cursor-pointer transition duration-300 hover:opacity-80'
)}
name={'pencil'}
onClick={e => {
// stopPropagation needed to avoid disable the current active item
e.stopPropagation();
onEdit(segmentationId, segmentIndex);
}}
/>
</div>
</div>
</div>
</div> */}
</div>
);
};
SegmentItem.propTypes = {
segmentIndex: PropTypes.number.isRequired,
segmentationId: PropTypes.string.isRequired,
label: PropTypes.string,
// color as array
color: PropTypes.array,
isActive: PropTypes.bool.isRequired,
isVisible: PropTypes.bool.isRequired,
isLocked: PropTypes.bool,
onClick: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onToggleVisibility: PropTypes.func.isRequired,
onToggleLocked: PropTypes.func,
};
SegmentItem.defaultProps = {
isActive: false,
};
export default SegmentItem;
@@ -0,0 +1,173 @@
import React from 'react';
import PropTypes from 'prop-types';
import Icon from '../Icon';
import SegmentationGroup from './SegmentationGroup';
import SegmentationConfig from './SegmentationConfig';
const GetSegmentationConfig = ({
setFillAlpha,
setFillAlphaInactive,
setOutlineWidthActive,
setRenderFill,
setRenderInactiveSegmentations,
setRenderOutline,
setOutlineOpacityActive,
segmentationConfig,
}) => {
return (
<SegmentationConfig
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
segmentationConfig={segmentationConfig}
/>
);
};
const SegmentationGroupTable = ({
segmentations,
onSegmentationAdd,
onSegmentationEdit,
onSegmentationClick,
onSegmentationDelete,
showAddSegmentation,
showAddSegment,
onSegmentClick,
onSegmentAdd,
segmentationConfig,
onSegmentDelete,
onSegmentEdit,
onToggleSegmentationVisibility,
onToggleSegmentVisibility,
onSegmentColorClick,
isMinimized,
onToggleMinimizeSegmentation,
setFillAlpha,
setFillAlphaInactive,
setOutlineWidthActive,
setOutlineOpacityActive,
setRenderFill,
setRenderInactiveSegmentations,
setRenderOutline,
}) => {
return (
<div className="font-inter font-[300]">
<GetSegmentationConfig
// showAddSegmentation={showAddSegmentation}
// onSegmentationAdd={onSegmentationAdd}
segmentationConfig={segmentationConfig}
setFillAlpha={setFillAlpha}
setFillAlphaInactive={setFillAlphaInactive}
setOutlineWidthActive={setOutlineWidthActive}
setOutlineOpacityActive={setOutlineOpacityActive}
setRenderFill={setRenderFill}
setRenderInactiveSegmentations={setRenderInactiveSegmentations}
setRenderOutline={setRenderOutline}
/>
<div className="flex flex-col pr-[1px] mt-1">
{!!segmentations.length &&
segmentations.map((segmentation, index) => {
const {
id,
label,
displayText = [],
segmentCount,
segments,
isVisible,
isActive,
activeSegmentIndex,
} = segmentation;
return (
<>
<SegmentationGroup
id={id}
label={label}
isMinimized={isMinimized[id]}
segments={segments}
showAddSegment={showAddSegment}
segmentCount={segmentCount}
isActive={isActive}
isVisible={isVisible}
onSegmentColorClick={onSegmentColorClick}
onSegmentationClick={() => onSegmentationClick(id)}
activeSegmentIndex={activeSegmentIndex}
onToggleMinimizeSegmentation={onToggleMinimizeSegmentation}
onSegmentationEdit={onSegmentationEdit}
onSegmentationDelete={onSegmentationDelete}
onSegmentClick={onSegmentClick}
onSegmentEdit={onSegmentEdit}
onToggleSegmentVisibility={onToggleSegmentVisibility}
onToggleSegmentationVisibility={
onToggleSegmentationVisibility
}
onSegmentAdd={onSegmentAdd}
showSegmentDelete={false}
/>
<div className="h-2 bg-black"></div>
</>
);
})}
</div>
{showAddSegmentation && (
<div
className="flex items-center cursor-pointer hover:opacity-80 text-primary-active bg-black text-[12px] pl-1 h-[45px]"
onClick={() => onSegmentationAdd()}
>
<Icon name="row-add" className="w-5 h-5" />
<div className="pl-1">Add New Segmentation</div>
</div>
)}
</div>
);
};
SegmentationGroupTable.propTypes = {
title: PropTypes.string.isRequired,
segmentations: PropTypes.array.isRequired,
activeSegmentationId: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onToggleLocked: PropTypes.func,
onToggleVisibility: PropTypes.func.isRequired,
onToggleVisibilityAll: PropTypes.func.isRequired,
segmentationConfig: PropTypes.object,
};
SegmentationGroupTable.defaultProps = {
title: '',
segmentations: [],
activeSegmentationId: '',
onClick: () => {},
onEdit: () => {},
onDelete: () => {},
onToggleLocked: () => {},
onToggleVisibility: () => {},
onToggleVisibilityAll: () => {},
segmentationConfig: {
initialConfig: {
fillAlpha: 0.5,
fillAlphaInactive: 0.5,
outlineWidthActive: 1,
outlineOpacity: 1,
outlineOpacityInactive: 0.5,
renderFill: true,
renderInactiveSegmentations: true,
renderOutline: true,
},
usePercentage: true,
},
setFillAlpha: () => {},
setFillAlphaInactive: () => {},
setOutlineWidthActive: () => {},
setOutlineOpacityActive: () => {},
setRenderFill: () => {},
setRenderInactiveSegmentations: () => {},
setRenderOutline: () => {},
};
export default SegmentationGroupTable;
@@ -0,0 +1,141 @@
import SegmentationGroupTable from '../SegmentationGroupTable';
import { ArgsTable, Story, Canvas, Meta } from '@storybook/addon-docs';
import { createComponentTemplate } from '../../../storybook/functions/create-component-story';
export const argTypes = {
component: SegmentationGroupTable,
title: 'Components/SegmentationGroupTable',
};
<Meta
title="Components/SegmentationGroupTable"
component={SegmentationGroupTable}
/>
export const SegmentationGroupTableTemplate = args => (
<div className="w-64 h-96 bg-primary-dark">
<SegmentationGroupTable {...args} />
</div>
);
<Heading
title="SegmentationGroupTable"
componentRelativePath="SegmentationGroupTable/SegmentationGroupTable.tsx"
/>
- [Overview](#overview)
- [Props](#props)
- [Contribute](#contribute)
## Overview
SegmentationGroupTable is a component that renders the SegmentationGroupTables.
<Canvas>
<Story
name="Overview"
args={{
activeSegmentationId: 'ed8d6f8d-4035-438d-9032-36d5912aaa3a',
showAddSegmentation: true,
isMinimized: {"4be5-b9b2-ed33-fa86970568e7": false},
showAddSegment: true,
segmentations: [
{
"id": "4be5-b9b2-ed33-fa86970568e7",
"volumeId": "a3b0f15f-4be5-b9b2-ed33-fa86970568e7",
"activeSegmentIndex": 1,
"cachedStats": {},
"label": "Segmentation",
"segmentsLocked": {},
"type": "LABELMAP",
"displayText": [],
"hydrated": true,
"segmentCount": 3,
"segments": [
null,
{
"label": "Segment 1",
"segmentIndex": 1,
"color": [
128,
174,
128
],
"opacity": 255,
"isVisible": true,
"isLocked": false,
},
{
"label": "Segment 2",
"segmentIndex": 2,
"color": [
241,
214,
145
],
"opacity": 255,
"isVisible": true,
"isLocked": false,
},
{
"label": "Segment 3",
"segmentIndex": 3,
"color": [
141,
114,
145
],
"opacity": 255,
"isVisible": false,
"isLocked": false
}
],
"isActive": false,
"colorLUTIndex": 1,
"isVisible": true
},
{
"id": "a3b0f15f-4be5-b9b2-ed33-fa86970568e7",
"volumeId": "a3b0f15f-4be5-b9b2-ed33-fa86970568e7",
"activeSegmentIndex": 1,
"cachedStats": {},
"label": "Segmentation2",
"segmentsLocked": {},
"type": "LABELMAP",
"displayText": [],
"hydrated": true,
"segmentCount": 2,
"segments": [
null,
{
"label": "Segment 1",
"segmentIndex": 1,
"color": [
228,
174,
128
],
"opacity": 255,
"isVisible": true,
"isLocked": false,
},
],
"isActive": true,
"colorLUTIndex": 1,
"isVisible": true
}
]
}}
>
{SegmentationGroupTableTemplate.bind({})}
</Story>
</Canvas>
## Props
<ArgsTable of={SegmentationGroupTable} />
## Contribute
<Footer componentRelativePath="SegmentationGroupTable/__stories__/SegmentationGroupTable.stories.mdx" />
@@ -0,0 +1,3 @@
import SegmentationGroupTable from './SegmentationGroupTable';
export default SegmentationGroupTable;
Loaded 100 of 254 files, more files were not shown because too many files have changed in this diff. Show more