Integration TrizRis OHIF: key images, annotations, layouts, access control and reconstruction
Build and Deploy Docs / build-and-deploy-docs (push) Canceled after 0s
Build and Deploy Docs / build-and-deploy-docs (push) Canceled after 0s
This commit is contained in:
1 parent
bbe37ff388
commit
f00379a672
28 files changed
+1898
-105
No files matched your search
@@ -4,6 +4,7 @@ import { Enums, eventTarget, getEnabledElement } from '@cornerstonejs/core';
|
||||
import { MeasurementService, useViewportRef } from '@ohif/core';
|
||||
import { useViewportDialog } from '@ohif/ui-next';
|
||||
import type { Types as csTypes } from '@cornerstonejs/core';
|
||||
import { deleteKeyImageFromTrizRis } from '../utils/keyImages/deleteKeyImageFromTrizRis';
|
||||
|
||||
import { setEnabledElement } from '../state';
|
||||
|
||||
@@ -17,6 +18,13 @@ import { getViewportPresentations } from '../utils/presentations/getViewportPres
|
||||
import { useSynchronizersStore } from '../stores/useSynchronizersStore';
|
||||
import ActiveViewportBehavior from '../utils/ActiveViewportBehavior';
|
||||
import { WITH_NAVIGATION } from '../services/ViewportService/CornerstoneViewportService';
|
||||
import { useKeyImagesStore } from '../stores/useKeyImagesStore';
|
||||
import { loadKeyImagesFromTrizRis } from '../utils/keyImages/loadKeyImagesFromTrizRis';
|
||||
import { useSavedKeyImages } from '../hooks/useSavedKeyImages';
|
||||
import {
|
||||
saveImageViewState,
|
||||
getImageViewState,
|
||||
} from '../../../default/src/services/TrizRisImageViewStateApi';
|
||||
|
||||
const STACK = 'stack';
|
||||
|
||||
@@ -80,7 +88,13 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
|
||||
const [scrollbarHeight, setScrollbarHeight] = useState('100px');
|
||||
const [enabledVPElement, setEnabledVPElement] = useState(null);
|
||||
const [currentImageId, setCurrentImageId] = useState<string | null>(null);
|
||||
const { savedKeyImageSOPs, removeSavedKeyImage, addSavedKeyImage } = useSavedKeyImages();
|
||||
const keyImages = useKeyImagesStore(state => state.keyImages);
|
||||
const addKeyImage = useKeyImagesStore(state => state.addKeyImage);
|
||||
const removeKeyImage = useKeyImagesStore(state => state.removeKeyImage);
|
||||
const elementRef = useRef() as React.MutableRefObject<HTMLDivElement>;
|
||||
|
||||
const viewportRef = useViewportRef(viewportId);
|
||||
|
||||
const {
|
||||
@@ -142,6 +156,201 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
};
|
||||
}, [onResize]);
|
||||
|
||||
// AJOUTER ICI
|
||||
useEffect(() => {
|
||||
const element = enabledVPElement;
|
||||
let isRestoringViewState = true;
|
||||
let lastRestoredImageId: string | null = null;
|
||||
const lastSavedViewState = new Map<string, { zoom: number; rotation: number }>();
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateCurrentImage = () => {
|
||||
try {
|
||||
const enabledElement = getEnabledElement(element);
|
||||
const viewport = enabledElement?.viewport;
|
||||
const imageId = viewport?.getCurrentImageId?.();
|
||||
|
||||
if (imageId) {
|
||||
setCurrentImageId(imageId);
|
||||
|
||||
if (lastRestoredImageId === imageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastRestoredImageId = imageId;
|
||||
isRestoringViewState = true;
|
||||
|
||||
const match = imageId.match(/studies\/([^/]+)\/series\/([^/]+)\/instances\/([^/]+)/);
|
||||
|
||||
if (match) {
|
||||
const studyInstanceUID = match[1];
|
||||
const seriesInstanceUID = match[2];
|
||||
const sopInstanceUID = match[3];
|
||||
|
||||
getImageViewState(studyInstanceUID, seriesInstanceUID, sopInstanceUID)
|
||||
.then(state => {
|
||||
console.log('### VIEW STATE RECUPERE ###');
|
||||
console.log(state);
|
||||
|
||||
if (state) {
|
||||
const imageKey = `${state.studyInstanceUID}|${state.seriesInstanceUID}|${state.sopInstanceUID}`;
|
||||
|
||||
lastSavedViewState.set(imageKey, {
|
||||
zoom: state.zoom,
|
||||
rotation: state.rotation,
|
||||
});
|
||||
const enabledElement = getEnabledElement(element);
|
||||
const viewport = enabledElement?.viewport;
|
||||
|
||||
if (!viewport) {
|
||||
// ===== CORRECTION 1 =====
|
||||
isRestoringViewState = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const currentImageId = viewport.getCurrentImageId?.();
|
||||
|
||||
if (currentImageId !== imageId) {
|
||||
// ===== CORRECTION 2 =====
|
||||
isRestoringViewState = false;
|
||||
return;
|
||||
}
|
||||
|
||||
viewport.setZoom(state.zoom);
|
||||
|
||||
viewport.setViewPresentation({
|
||||
rotation: state.rotation,
|
||||
});
|
||||
|
||||
viewport.render();
|
||||
|
||||
console.log('### VIEW STATE APPLIQUE ###');
|
||||
console.log('zoom =', state.zoom);
|
||||
console.log('rotation =', state.rotation);
|
||||
}
|
||||
|
||||
// ===== CORRECTION 3 =====
|
||||
// Important : même si state == null,
|
||||
// on réactive la sauvegarde
|
||||
isRestoringViewState = false;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur récupération zoom/rotation :', error);
|
||||
|
||||
// ===== CORRECTION 4 =====
|
||||
// Si le GET échoue, on ne reste pas bloqué
|
||||
isRestoringViewState = false;
|
||||
});
|
||||
} else {
|
||||
// ===== CORRECTION 5 =====
|
||||
// Si l'imageId ne correspond pas au regex,
|
||||
// on réactive quand même la sauvegarde
|
||||
isRestoringViewState = false;
|
||||
}
|
||||
|
||||
console.log('### IMAGE COURANTE VIEWPORT', viewportId, '###');
|
||||
|
||||
console.log(imageId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur récupération image courante :', error);
|
||||
|
||||
// ===== CORRECTION 6 =====
|
||||
isRestoringViewState = false;
|
||||
}
|
||||
};
|
||||
|
||||
updateCurrentImage();
|
||||
let saveViewStateTimeout: ReturnType<typeof setTimeout>;
|
||||
const handleCameraModified = () => {
|
||||
clearTimeout(saveViewStateTimeout);
|
||||
|
||||
saveViewStateTimeout = setTimeout(() => {
|
||||
if (isRestoringViewState) {
|
||||
console.log('### SAUVEGARDE IGNOREE - RESTAURATION EN COURS ###');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const enabledElement = getEnabledElement(element);
|
||||
const viewport = enabledElement?.viewport;
|
||||
|
||||
if (!viewport) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageId = viewport.getCurrentImageId?.();
|
||||
|
||||
if (!imageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoom = viewport.getZoom();
|
||||
const rotation = viewport.getRotation();
|
||||
|
||||
console.log('### CAMERA MODIFIED TERMINE ###');
|
||||
console.log('zoom =', zoom);
|
||||
console.log('rotation =', rotation);
|
||||
|
||||
const match = imageId.match(/studies\/([^/]+)\/series\/([^/]+)\/instances\/([^/]+)/);
|
||||
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const studyInstanceUID = match[1];
|
||||
const seriesInstanceUID = match[2];
|
||||
const sopInstanceUID = match[3];
|
||||
|
||||
const imageKey = `${studyInstanceUID}|${seriesInstanceUID}|${sopInstanceUID}`;
|
||||
|
||||
const previousState = lastSavedViewState.get(imageKey);
|
||||
|
||||
if (
|
||||
previousState &&
|
||||
Math.abs(previousState.zoom - zoom) < 0.000001 &&
|
||||
previousState.rotation === rotation
|
||||
) {
|
||||
console.log('### VIEW STATE INCHANGE -> PAS DE SAUVEGARDE ###');
|
||||
return;
|
||||
}
|
||||
console.log('### SAUVEGARDE VIEW STATE ###');
|
||||
console.log('studyInstanceUID =', studyInstanceUID);
|
||||
console.log('seriesInstanceUID =', seriesInstanceUID);
|
||||
console.log('sopInstanceUID =', sopInstanceUID);
|
||||
saveImageViewState({
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID,
|
||||
zoom,
|
||||
rotation,
|
||||
})
|
||||
.then(() => {
|
||||
lastSavedViewState.set(imageKey, {
|
||||
zoom,
|
||||
rotation,
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur sauvegarde zoom/rotation :', error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Erreur récupération zoom/rotation :', error);
|
||||
}
|
||||
}, 400);
|
||||
};
|
||||
element.addEventListener(Enums.Events.IMAGE_RENDERED, updateCurrentImage);
|
||||
|
||||
element.addEventListener(Enums.Events.CAMERA_MODIFIED, handleCameraModified);
|
||||
|
||||
return () => {
|
||||
clearTimeout(saveViewStateTimeout);
|
||||
element.removeEventListener(Enums.Events.IMAGE_RENDERED, updateCurrentImage);
|
||||
element.removeEventListener(Enums.Events.CAMERA_MODIFIED, handleCameraModified);
|
||||
};
|
||||
}, [enabledVPElement, viewportId]);
|
||||
|
||||
const cleanUpServices = useCallback(
|
||||
viewportInfo => {
|
||||
const renderingEngineId = viewportInfo.getRenderingEngineId();
|
||||
@@ -304,11 +513,104 @@ const OHIFCornerstoneViewport = React.memo(
|
||||
loadViewportData();
|
||||
}, [viewportOptions, displaySets, dataSource]);
|
||||
|
||||
const getSopInstanceUIDFromImageId = (imageId: string | null) => {
|
||||
if (!imageId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = imageId.match(/\/instances\/([^/]+)/i);
|
||||
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
};
|
||||
|
||||
const currentSopInstanceUID = getSopInstanceUIDFromImageId(currentImageId);
|
||||
|
||||
const isSavedKeyImage = !!(
|
||||
currentSopInstanceUID && savedKeyImageSOPs.includes(currentSopInstanceUID)
|
||||
);
|
||||
|
||||
const isCurrentKeyImage = !!(currentImageId && (keyImages[currentImageId] || isSavedKeyImage));
|
||||
const toggleCurrentKeyImage = async () => {
|
||||
if (!currentImageId) {
|
||||
console.log('Aucune image actuellement affichée');
|
||||
return;
|
||||
}
|
||||
|
||||
const sopInstanceUID = getSopInstanceUIDFromImageId(currentImageId);
|
||||
|
||||
if (!sopInstanceUID) {
|
||||
console.error('SOP Instance UID introuvable');
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* L'image est actuellement Key Image
|
||||
* soit parce qu'elle est dans le store local,
|
||||
* soit parce qu'elle vient de la base TrizRis.
|
||||
*/
|
||||
if (isCurrentKeyImage) {
|
||||
console.log('### DESACTIVATION KEY IMAGE ###');
|
||||
|
||||
/*
|
||||
* Si elle était sauvegardée dans TrizRis,
|
||||
* supprimer fichier + base.
|
||||
*/
|
||||
if (isSavedKeyImage) {
|
||||
await deleteKeyImageFromTrizRis(currentImageId);
|
||||
|
||||
removeSavedKeyImage(sopInstanceUID);
|
||||
}
|
||||
|
||||
/*
|
||||
* Si elle existe aussi dans le store local,
|
||||
* on la retire explicitement.
|
||||
*/
|
||||
if (keyImages[currentImageId]) {
|
||||
removeKeyImage(currentImageId);
|
||||
}
|
||||
|
||||
console.log('### KEY IMAGE DESACTIVE ###');
|
||||
} else {
|
||||
/*
|
||||
* Elle n'était pas active :
|
||||
* on l'ajoute explicitement au store.
|
||||
*/
|
||||
addKeyImage(currentImageId, viewportId);
|
||||
|
||||
console.log('### KEY IMAGE ACTIVE ###');
|
||||
}
|
||||
};
|
||||
const Notification = customizationService.getCustomization('ui.notificationComponent');
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="viewport-wrapper">
|
||||
<button
|
||||
type="button"
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
toggleCurrentKeyImage();
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '35px',
|
||||
right: '10px',
|
||||
zIndex: 1000,
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
borderRadius: '50%',
|
||||
border: '1px solid #666',
|
||||
background: 'rgba(0, 0, 0, 0.65)',
|
||||
color: currentImageId && isCurrentKeyImage ? '#FFD700' : '#FFFFFF',
|
||||
fontSize: '22px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
title={
|
||||
currentImageId && isCurrentKeyImage ? 'Désactiver Key Image' : 'Activer Key Image'
|
||||
}
|
||||
>
|
||||
{currentImageId && isCurrentKeyImage ? '★' : '☆'}
|
||||
</button>
|
||||
<div
|
||||
className="cornerstone-viewport-element"
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
|
||||
@@ -37,6 +37,8 @@ import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/u
|
||||
import { getViewportEnabledElement } from './utils/getViewportEnabledElement';
|
||||
import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement';
|
||||
import toggleVOISliceSync from './utils/toggleVOISliceSync';
|
||||
import { useKeyImagesStore } from './stores/useKeyImagesStore';
|
||||
import { exportKeyImageToTrizRis } from './utils/keyImages/exportKeyImagesToTrizRis';
|
||||
import {
|
||||
isStackViewportType,
|
||||
isOrthographicViewportType,
|
||||
@@ -231,7 +233,31 @@ function commandsModule({
|
||||
}
|
||||
};
|
||||
};
|
||||
const extractDicomInfoFromImageId = (imageId: string) => {
|
||||
if (!imageId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageIdWithoutPrefix = imageId.replace(/^wadors:/, '').replace(/^dicomweb:/, '');
|
||||
|
||||
const match = imageIdWithoutPrefix.match(
|
||||
/\/studies\/([^/]+)\/series\/([^/]+)\/instances\/([^/]+)(?:\/frames\/(\d+))?/i
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
console.error('Impossible d extraire les UID depuis imageId :', imageId);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
imageId,
|
||||
studyInstanceUID: decodeURIComponent(match[1]),
|
||||
seriesInstanceUID: decodeURIComponent(match[2]),
|
||||
sopInstanceUID: decodeURIComponent(match[3]),
|
||||
frameNumber: match[4] || null,
|
||||
};
|
||||
};
|
||||
const actions = {
|
||||
jumpToMeasurementViewport: ({ annotationUID, measurement }) => {
|
||||
cornerstoneTools.annotation.selection.setAnnotationSelected(annotationUID, true);
|
||||
@@ -1136,6 +1162,34 @@ function commandsModule({
|
||||
bindings,
|
||||
});
|
||||
},
|
||||
//key image button
|
||||
toggleKeyImageMode: async () => {
|
||||
console.log('### KEY IMAGES BUTTON CLICKED ###');
|
||||
|
||||
const keyImages = useKeyImagesStore.getState().keyImages;
|
||||
|
||||
const liste = Object.values(keyImages);
|
||||
|
||||
console.log('Nombre de Key Images =', liste.length);
|
||||
|
||||
if (liste.length === 0) {
|
||||
console.log('Aucune Key Image sélectionnée');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('### EXPORT DE TOUTES LES KEY IMAGES ###');
|
||||
|
||||
for (let i = 0; i < liste.length; i++) {
|
||||
const keyImage = liste[i];
|
||||
|
||||
console.log(`Export ${i + 1} / ${liste.length}`);
|
||||
|
||||
await exportKeyImageToTrizRis(keyImage.imageId, keyImage.viewportId);
|
||||
}
|
||||
|
||||
console.log('### EXPORT KEY IMAGES TERMINE ###');
|
||||
},
|
||||
// capture viewport
|
||||
showDownloadViewportModal: () => {
|
||||
const { activeViewportId } = viewportGridService.getState();
|
||||
@@ -2700,6 +2754,9 @@ function commandsModule({
|
||||
deleteActiveAnnotation: {
|
||||
commandFn: actions.deleteActiveAnnotation,
|
||||
},
|
||||
toggleKeyImageMode: {
|
||||
commandFn: actions.toggleKeyImageMode,
|
||||
},
|
||||
setDisplaySetsForViewports: actions.setDisplaySetsForViewports,
|
||||
undo: actions.undo,
|
||||
redo: actions.redo,
|
||||
|
||||
@@ -38,6 +38,13 @@ const toolbarButtons: Button[] = [
|
||||
buttonSection: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Reconstruction',
|
||||
uiType: 'ohif.toolButtonList',
|
||||
props: {
|
||||
buttonSection: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'AdvancedRenderingControls',
|
||||
uiType: 'ohif.advancedRenderingControls',
|
||||
@@ -633,6 +640,86 @@ const toolbarButtons: Button[] = [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'MPR',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'layout-advanced-mpr',
|
||||
label: 'MPR',
|
||||
tooltip: 'Reconstruction multiplanaire',
|
||||
commands: {
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: 'mpr',
|
||||
},
|
||||
},
|
||||
evaluate: 'evaluate.action',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'MPR3D',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'layout-advanced-mpr',
|
||||
label: 'MPR + 3D',
|
||||
tooltip: 'Reconstruction multiplanaire avec rendu 3D',
|
||||
commands: {
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: 'mprAnd3DVolumeViewport',
|
||||
},
|
||||
},
|
||||
evaluate: 'evaluate.action',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'MIP',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'layout-advanced-mpr',
|
||||
label: 'MIP',
|
||||
tooltip: 'Maximum Intensity Projection',
|
||||
commands: {
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: 'mip',
|
||||
},
|
||||
},
|
||||
evaluate: 'evaluate.action',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'KeyImages',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'Export',
|
||||
label: 'Key Images',
|
||||
commands: 'toggleKeyImageMode',
|
||||
evaluate: [
|
||||
'evaluate.action',
|
||||
{
|
||||
name: 'evaluate.viewport.supported',
|
||||
unsupportedViewportTypes: ['video', 'wholeSlide'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Only3D',
|
||||
uiType: 'ohif.toolButton',
|
||||
props: {
|
||||
icon: 'layout-advanced-mpr',
|
||||
label: '3D',
|
||||
tooltip: 'Rendu volumique 3D',
|
||||
commands: {
|
||||
commandName: 'setHangingProtocol',
|
||||
commandOptions: {
|
||||
protocolId: 'only3D',
|
||||
},
|
||||
},
|
||||
evaluate: 'evaluate.action',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'Layout',
|
||||
uiType: 'ohif.layoutSelector',
|
||||
@@ -723,11 +810,13 @@ export const toolbarSections = {
|
||||
'TrackballRotate',
|
||||
'WindowLevel',
|
||||
'Capture',
|
||||
'KeyImages',
|
||||
'Layout',
|
||||
'Crosshairs',
|
||||
'MoreTools',
|
||||
'Reconstruction',
|
||||
],
|
||||
|
||||
Reconstruction: ['MPR', 'MPR3D', 'MIP', 'Only3D'],
|
||||
[TOOLBAR_SECTIONS.viewportActionMenu.topLeft]: ['orientationMenu', 'dataOverlayMenu'],
|
||||
|
||||
[TOOLBAR_SECTIONS.viewportActionMenu.bottomMiddle]: ['AdvancedRenderingControls'],
|
||||
|
||||
@@ -6,9 +6,14 @@ import { only3D } from './hps/only3D';
|
||||
import { primary3D } from './hps/primary3D';
|
||||
import { primaryAxial } from './hps/primaryAxial';
|
||||
import { frameView } from './hps/frameView';
|
||||
import { mip } from './hps/mip';
|
||||
|
||||
function getHangingProtocolModule() {
|
||||
return [
|
||||
{
|
||||
name: mip.id,
|
||||
protocol: mip,
|
||||
},
|
||||
{
|
||||
name: mpr.id,
|
||||
protocol: mpr,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { loadKeyImagesFromTrizRis } from '../utils/keyImages/loadKeyImagesFromTrizRis';
|
||||
|
||||
export const useSavedKeyImages = () => {
|
||||
const [savedKeyImageSOPs, setSavedKeyImageSOPs] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const chargerKeyImages = async () => {
|
||||
const sopInstanceUIDs = await loadKeyImagesFromTrizRis();
|
||||
|
||||
setSavedKeyImageSOPs(sopInstanceUIDs);
|
||||
};
|
||||
|
||||
chargerKeyImages();
|
||||
}, []);
|
||||
|
||||
const removeSavedKeyImage = (sopInstanceUID: string) => {
|
||||
setSavedKeyImageSOPs(prev => prev.filter(sop => sop !== sopInstanceUID));
|
||||
};
|
||||
|
||||
const addSavedKeyImage = (sopInstanceUID: string) => {
|
||||
setSavedKeyImageSOPs(prev => {
|
||||
if (prev.includes(sopInstanceUID)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return [...prev, sopInstanceUID];
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
savedKeyImageSOPs,
|
||||
removeSavedKeyImage,
|
||||
addSavedKeyImage,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import { HYDRATE_SEG_SYNC_GROUP, VOI_SYNC_GROUP } from './mpr';
|
||||
|
||||
export const mip = {
|
||||
id: 'mip',
|
||||
locked: true,
|
||||
name: 'MIP',
|
||||
|
||||
createdDate: '2026-10-08T00:00:00.000Z',
|
||||
modifiedDate: '2026-10-08T00:00:00.000Z',
|
||||
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
|
||||
protocolMatchingRules: [],
|
||||
|
||||
imageLoadStrategy: 'interleaveCenter',
|
||||
|
||||
displaySetSelectors: {
|
||||
activeDisplaySet: {
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
weight: 1,
|
||||
attribute: 'isReconstructable',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
stages: [
|
||||
{
|
||||
id: 'mipStage',
|
||||
name: 'MIP',
|
||||
|
||||
viewportStructure: {
|
||||
layoutType: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 1,
|
||||
},
|
||||
},
|
||||
|
||||
viewports: [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'mip-axial',
|
||||
toolGroupId: 'mpr',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
|
||||
initialImageOptions: {
|
||||
preset: 'middle',
|
||||
},
|
||||
|
||||
syncGroups: [VOI_SYNC_GROUP, HYDRATE_SEG_SYNC_GROUP],
|
||||
},
|
||||
|
||||
displaySets: [
|
||||
{
|
||||
id: 'activeDisplaySet',
|
||||
options: {
|
||||
blendMode: 'MIP',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type KeyImageInfo = {
|
||||
imageId: string;
|
||||
viewportId?: string;
|
||||
};
|
||||
|
||||
type KeyImagesState = {
|
||||
keyImages: Record<string, KeyImageInfo>;
|
||||
|
||||
addKeyImage: (imageId: string, viewportId?: string) => void;
|
||||
|
||||
removeKeyImage: (imageId: string) => void;
|
||||
|
||||
toggleKeyImage: (imageId: string, viewportId?: string) => void;
|
||||
|
||||
isKeyImage: (imageId: string) => boolean;
|
||||
|
||||
clearKeyImages: () => void;
|
||||
};
|
||||
|
||||
export const useKeyImagesStore = create<KeyImagesState>((set, get) => ({
|
||||
keyImages: {},
|
||||
|
||||
addKeyImage: (imageId, viewportId) => {
|
||||
set(state => ({
|
||||
keyImages: {
|
||||
...state.keyImages,
|
||||
[imageId]: {
|
||||
imageId,
|
||||
viewportId,
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
console.log('### KEY IMAGE AJOUTEE ###', imageId);
|
||||
},
|
||||
|
||||
removeKeyImage: imageId => {
|
||||
set(state => {
|
||||
const newKeyImages = {
|
||||
...state.keyImages,
|
||||
};
|
||||
|
||||
delete newKeyImages[imageId];
|
||||
|
||||
return {
|
||||
keyImages: newKeyImages,
|
||||
};
|
||||
});
|
||||
|
||||
console.log('### KEY IMAGE RETIREE ###', imageId);
|
||||
},
|
||||
|
||||
toggleKeyImage: (imageId, viewportId) => {
|
||||
const existe = !!get().keyImages[imageId];
|
||||
|
||||
if (existe) {
|
||||
get().removeKeyImage(imageId);
|
||||
} else {
|
||||
get().addKeyImage(imageId, viewportId);
|
||||
}
|
||||
},
|
||||
|
||||
isKeyImage: imageId => {
|
||||
return !!get().keyImages[imageId];
|
||||
},
|
||||
|
||||
clearKeyImages: () => {
|
||||
set({
|
||||
keyImages: {},
|
||||
});
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,32 @@
|
||||
import { extractDicomInfoFromImageId } from './extractDicomInfoFromImageId';
|
||||
import { TrizRisConfig } from '@ohif/core/src/config/TrizRisConfig';
|
||||
|
||||
export const deleteKeyImageFromTrizRis = async (imageId: string) => {
|
||||
const dicomInfo = extractDicomInfoFromImageId(imageId);
|
||||
|
||||
if (!dicomInfo) {
|
||||
console.error('Informations DICOM introuvables');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`${TrizRisConfig.apiBaseUrl}/dicom/key-image`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-SOP-Instance-UID': dicomInfo.sopInstanceUID,
|
||||
|
||||
...(dicomInfo.frameNumber
|
||||
? {
|
||||
'X-Frame-Number': dicomInfo.frameNumber,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
const resultat = await response.text();
|
||||
|
||||
console.log('### SUPPRESSION KEY IMAGE TRIZRIS ###');
|
||||
|
||||
console.log('HTTP =', response.status);
|
||||
|
||||
console.log('Résultat =', resultat);
|
||||
};
|
||||
@@ -0,0 +1,322 @@
|
||||
import html2canvas from 'html2canvas';
|
||||
|
||||
import { Enums, getEnabledElement, getRenderingEngine } from '@cornerstonejs/core';
|
||||
|
||||
import { getEnabledElement as OHIFgetEnabledElement } from '../../state';
|
||||
|
||||
import { extractDicomInfoFromImageId } from './extractDicomInfoFromImageId';
|
||||
|
||||
import { ToolGroupManager } from '@cornerstonejs/tools';
|
||||
|
||||
import { TrizRisConfig } from '@ohif/core/src/config/TrizRisConfig';
|
||||
|
||||
const attendreRenduImage = (element: HTMLElement) => {
|
||||
return new Promise<void>(resolve => {
|
||||
let termine = false;
|
||||
|
||||
const terminer = () => {
|
||||
if (termine) {
|
||||
return;
|
||||
}
|
||||
|
||||
termine = true;
|
||||
|
||||
element.removeEventListener(Enums.Events.IMAGE_RENDERED, handler as EventListener);
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
const handler = () => {
|
||||
terminer();
|
||||
};
|
||||
|
||||
element.addEventListener(Enums.Events.IMAGE_RENDERED, handler as EventListener);
|
||||
|
||||
// Sécurité si jamais l'événement ne remonte pas
|
||||
setTimeout(() => {
|
||||
terminer();
|
||||
}, 1500);
|
||||
});
|
||||
};
|
||||
|
||||
export const exportKeyImageToTrizRis = async (imageId: string, viewportId: string) => {
|
||||
console.log('====================================');
|
||||
console.log('### DEBUT EXPORT KEY IMAGE ###');
|
||||
console.log('ImageId demandé =', imageId);
|
||||
console.log('Viewport source =', viewportId);
|
||||
|
||||
const dicomInfo = extractDicomInfoFromImageId(imageId);
|
||||
|
||||
if (!dicomInfo) {
|
||||
console.error('Informations DICOM introuvables');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* ------------------------------------------------
|
||||
* 1. Retrouver le viewport OHIF visible
|
||||
* ------------------------------------------------
|
||||
*
|
||||
* On l'utilise uniquement pour retrouver
|
||||
* le RenderingEngine et le type du viewport.
|
||||
*
|
||||
* ON NE MODIFIE PLUS SON IMAGE.
|
||||
*/
|
||||
const enabledElementOHIF = OHIFgetEnabledElement(viewportId);
|
||||
|
||||
if (!enabledElementOHIF?.element) {
|
||||
console.error('Impossible de trouver le viewport OHIF :', viewportId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceElement = enabledElementOHIF.element as HTMLElement;
|
||||
|
||||
const sourceEnabledElement = getEnabledElement(sourceElement);
|
||||
|
||||
if (!sourceEnabledElement) {
|
||||
console.error('EnabledElement Cornerstone source introuvable');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceViewport = sourceEnabledElement.viewport;
|
||||
|
||||
const renderingEngineId = sourceEnabledElement.renderingEngineId;
|
||||
|
||||
console.log('RenderingEngine ID =', renderingEngineId);
|
||||
|
||||
console.log('Image visible utilisateur =', sourceViewport.getCurrentImageId?.());
|
||||
|
||||
const sourceToolGroup = ToolGroupManager.getToolGroupForViewport(
|
||||
sourceViewport.id,
|
||||
renderingEngineId
|
||||
);
|
||||
|
||||
if (!sourceToolGroup) {
|
||||
console.warn('Aucun ToolGroup trouvé pour le viewport source');
|
||||
}
|
||||
|
||||
/*
|
||||
* ------------------------------------------------
|
||||
* 2. Retrouver le RenderingEngine
|
||||
* ------------------------------------------------
|
||||
*/
|
||||
const renderingEngine = getRenderingEngine(renderingEngineId);
|
||||
|
||||
if (!renderingEngine) {
|
||||
console.error('RenderingEngine introuvable');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* ------------------------------------------------
|
||||
* 3. Créer un viewport caché hors écran
|
||||
* ------------------------------------------------
|
||||
*/
|
||||
const hiddenViewportId = `key-image-export-${Date.now()}-${Math.random()
|
||||
.toString(36)
|
||||
.substring(2)}`;
|
||||
|
||||
const hiddenElement = document.createElement('div');
|
||||
|
||||
hiddenElement.setAttribute('data-key-image-viewport', hiddenViewportId);
|
||||
|
||||
hiddenElement.style.position = 'fixed';
|
||||
|
||||
/*
|
||||
* IMPORTANT :
|
||||
* ne pas utiliser display:none.
|
||||
*
|
||||
* Cornerstone doit réellement pouvoir rendre
|
||||
* dans le div.
|
||||
*/
|
||||
hiddenElement.style.left = '-10000px';
|
||||
hiddenElement.style.top = '0';
|
||||
|
||||
hiddenElement.style.width = '512px';
|
||||
hiddenElement.style.height = '512px';
|
||||
|
||||
hiddenElement.style.pointerEvents = 'none';
|
||||
|
||||
hiddenElement.style.background = 'black';
|
||||
|
||||
document.body.appendChild(hiddenElement);
|
||||
|
||||
try {
|
||||
/*
|
||||
* ------------------------------------------------
|
||||
* 4. Activer le viewport caché Cornerstone
|
||||
* ------------------------------------------------
|
||||
*/
|
||||
console.log('### CREATION VIEWPORT CACHE ###');
|
||||
|
||||
renderingEngine.enableElement({
|
||||
viewportId: hiddenViewportId,
|
||||
|
||||
element: hiddenElement,
|
||||
|
||||
/*
|
||||
* On utilise le même type de viewport
|
||||
* que le viewport source.
|
||||
*/
|
||||
type: sourceViewport.type,
|
||||
|
||||
defaultOptions: {
|
||||
background: sourceViewport.defaultOptions?.background,
|
||||
|
||||
orientation: sourceViewport.defaultOptions?.orientation,
|
||||
},
|
||||
});
|
||||
|
||||
if (sourceToolGroup) {
|
||||
sourceToolGroup.addViewport(hiddenViewportId, renderingEngineId);
|
||||
}
|
||||
|
||||
const captureViewport = renderingEngine.getViewport(hiddenViewportId);
|
||||
|
||||
if (!captureViewport) {
|
||||
console.error('Viewport caché introuvable');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* ------------------------------------------------
|
||||
* 5. Charger LA Key Image dans le viewport caché
|
||||
* ------------------------------------------------
|
||||
*/
|
||||
console.log('### CHARGEMENT KEY IMAGE DANS VIEWPORT CACHE ###');
|
||||
|
||||
const renderPromise = attendreRenduImage(hiddenElement);
|
||||
|
||||
await captureViewport.setStack([imageId], 0);
|
||||
|
||||
captureViewport.render();
|
||||
|
||||
await renderPromise;
|
||||
|
||||
const imageChargee = captureViewport.getCurrentImageId?.();
|
||||
|
||||
console.log('Image chargée dans viewport caché =', imageChargee);
|
||||
|
||||
/*
|
||||
* Sécurité :
|
||||
* vérifier qu'on capture bien la bonne image.
|
||||
*/
|
||||
if (imageChargee !== imageId) {
|
||||
console.error('ERREUR : le viewport caché n affiche pas la Key Image demandée');
|
||||
|
||||
console.error('Demandée =', imageId);
|
||||
|
||||
console.error('Affichée =', imageChargee);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* ------------------------------------------------
|
||||
* 6. Transformer le viewport caché en PNG
|
||||
* ------------------------------------------------
|
||||
*/
|
||||
console.log('### CREATION PNG ###');
|
||||
|
||||
const canvas = await html2canvas(hiddenElement);
|
||||
|
||||
const blob = await new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
blob => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error('Impossible de créer le PNG'));
|
||||
}
|
||||
},
|
||||
|
||||
'image/png',
|
||||
|
||||
1.0
|
||||
);
|
||||
});
|
||||
|
||||
console.log('### PNG KEY IMAGE CREE ###');
|
||||
|
||||
console.log('StudyInstanceUID =', dicomInfo.studyInstanceUID);
|
||||
|
||||
console.log('SeriesInstanceUID =', dicomInfo.seriesInstanceUID);
|
||||
|
||||
console.log('SOPInstanceUID =', dicomInfo.sopInstanceUID);
|
||||
|
||||
console.log('FrameNumber =', dicomInfo.frameNumber);
|
||||
|
||||
console.log('Taille PNG =', blob.size);
|
||||
|
||||
/*
|
||||
* ------------------------------------------------
|
||||
* 7. Envoyer vers TrizRis
|
||||
* ------------------------------------------------
|
||||
*/
|
||||
console.log('### ENVOI VERS TRIZRIS ###');
|
||||
|
||||
const response = await fetch(`${TrizRisConfig.apiBaseUrl}/dicom/save-image`, {
|
||||
method: 'POST',
|
||||
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
|
||||
'X-Study-Instance-UID': dicomInfo.studyInstanceUID,
|
||||
|
||||
'X-Series-Instance-UID': dicomInfo.seriesInstanceUID,
|
||||
|
||||
'X-SOP-Instance-UID': dicomInfo.sopInstanceUID,
|
||||
|
||||
...(dicomInfo.frameNumber
|
||||
? {
|
||||
'X-Frame-Number': dicomInfo.frameNumber,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
|
||||
body: blob,
|
||||
});
|
||||
|
||||
const resultat = await response.text();
|
||||
|
||||
console.log('### REPONSE TRIZRIS ###');
|
||||
|
||||
console.log('HTTP =', response.status);
|
||||
|
||||
console.log('Résultat =', resultat);
|
||||
} catch (error) {
|
||||
console.error('Erreur export Key Image :', error);
|
||||
} finally {
|
||||
/*
|
||||
* ------------------------------------------------
|
||||
* 8. Détruire le viewport caché
|
||||
* ------------------------------------------------
|
||||
*/
|
||||
console.log('### SUPPRESSION VIEWPORT CACHE ###');
|
||||
if (sourceToolGroup) {
|
||||
try {
|
||||
sourceToolGroup.removeViewports(renderingEngineId, hiddenViewportId);
|
||||
} catch (error) {
|
||||
console.warn('Erreur suppression viewport caché du ToolGroup', error);
|
||||
}
|
||||
}
|
||||
try {
|
||||
renderingEngine.disableElement(hiddenViewportId);
|
||||
} catch (error) {
|
||||
console.warn('Erreur lors de la désactivation du viewport caché :', error);
|
||||
}
|
||||
|
||||
if (hiddenElement.parentNode) {
|
||||
hiddenElement.parentNode.removeChild(hiddenElement);
|
||||
}
|
||||
|
||||
console.log('### FIN EXPORT KEY IMAGE ###');
|
||||
|
||||
console.log('====================================');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
export const extractDicomInfoFromImageId = (imageId: string) => {
|
||||
if (!imageId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageIdWithoutPrefix = imageId.replace(/^wadors:/, '').replace(/^dicomweb:/, '');
|
||||
|
||||
const match = imageIdWithoutPrefix.match(
|
||||
/\/studies\/([^/]+)\/series\/([^/]+)\/instances\/([^/]+)(?:\/frames\/(\d+))?/i
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
console.error('Impossible d extraire les UID depuis imageId :', imageId);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
imageId,
|
||||
studyInstanceUID: decodeURIComponent(match[1]),
|
||||
seriesInstanceUID: decodeURIComponent(match[2]),
|
||||
sopInstanceUID: decodeURIComponent(match[3]),
|
||||
frameNumber: match[4] || null,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TrizRisConfig } from '@ohif/core/src/config/TrizRisConfig';
|
||||
|
||||
export const loadKeyImagesFromTrizRis = async (): Promise<string[]> => {
|
||||
try {
|
||||
const response = await fetch(`${TrizRisConfig.apiBaseUrl}/dicom/key-images`);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Erreur chargement Key Images :', response.status);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
/*
|
||||
* Ton endpoint renvoie actuellement du XML :
|
||||
*
|
||||
* <List>
|
||||
* <item>...</item>
|
||||
* </List>
|
||||
*/
|
||||
|
||||
const parser = new DOMParser();
|
||||
|
||||
const xml = parser.parseFromString(text, 'application/xml');
|
||||
|
||||
const items = Array.from(xml.getElementsByTagName('item'));
|
||||
|
||||
const sopInstanceUIDs = items.map(item => item.textContent?.trim()).filter(Boolean) as string[];
|
||||
|
||||
console.log('### KEY IMAGES CHARGEES DEPUIS TRIZRIS ###');
|
||||
|
||||
console.log(sopInstanceUIDs);
|
||||
|
||||
return sopInstanceUIDs;
|
||||
} catch (error) {
|
||||
console.error('Erreur récupération Key Images :', error);
|
||||
|
||||
return [];
|
||||
}
|
||||
};
|
||||
@@ -28,6 +28,7 @@ import { useViewportsByPositionStore } from './stores/useViewportsByPositionStor
|
||||
import { useToggleOneUpViewportGridStore } from './stores/useToggleOneUpViewportGridStore';
|
||||
import requestDisplaySetCreationForStudy from './Panels/requestDisplaySetCreationForStudy';
|
||||
import promptSaveReport from './utils/promptSaveReport';
|
||||
import { saveLayoutPreference } from './services/TrizRisLayoutApi';
|
||||
|
||||
export type HangingProtocolParams = {
|
||||
protocolId?: string;
|
||||
@@ -498,12 +499,21 @@ const commandsModule = ({
|
||||
/**
|
||||
* Changes the viewport grid layout in terms of the MxN layout.
|
||||
*/
|
||||
setViewportGridLayout: ({ numRows, numCols, isHangingProtocolLayout = false }) => {
|
||||
const { protocol } = hangingProtocolService.getActiveProtocol();
|
||||
const onLayoutChange = protocol.callbacks?.onLayoutChange;
|
||||
if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) {
|
||||
// Don't apply the layout if the run command returns false
|
||||
return;
|
||||
setViewportGridLayout: ({
|
||||
numRows,
|
||||
numCols,
|
||||
isHangingProtocolLayout = false,
|
||||
savePreference = true,
|
||||
}) => {
|
||||
const activeProtocol = hangingProtocolService.getActiveProtocol();
|
||||
const protocol = activeProtocol?.protocol;
|
||||
|
||||
const onLayoutChange = protocol?.callbacks?.onLayoutChange;
|
||||
|
||||
if (onLayoutChange) {
|
||||
if (commandsManager.run(onLayoutChange, { numRows, numCols }) === false) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const completeLayout = () => {
|
||||
@@ -525,9 +535,49 @@ const commandsModule = ({
|
||||
findOrCreateViewport,
|
||||
isHangingProtocolLayout,
|
||||
});
|
||||
|
||||
console.log('### LAYOUT CHOISI ###');
|
||||
console.log('numRows =', numRows);
|
||||
console.log('numCols =', numCols);
|
||||
console.log('isHangingProtocolLayout =', isHangingProtocolLayout);
|
||||
|
||||
if (!isHangingProtocolLayout && savePreference) {
|
||||
const hangingProtocolState = hangingProtocolService.getState();
|
||||
|
||||
const studyInstanceUID = hangingProtocolState.activeStudyUID;
|
||||
|
||||
console.log('### STUDY UID POUR LAYOUT ###', studyInstanceUID);
|
||||
if (!studyInstanceUID) {
|
||||
console.error('StudyInstanceUID introuvable dans URL');
|
||||
return;
|
||||
}
|
||||
saveLayoutPreference(numRows, numCols, studyInstanceUID)
|
||||
.then(() => {
|
||||
console.log('### LAYOUT SAUVEGARDE AVEC SUCCES ###');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('### ERREUR SAUVEGARDE LAYOUT ###', error);
|
||||
});
|
||||
}
|
||||
};
|
||||
// Need to finish any work in the callback
|
||||
window.setTimeout(completeLayout, 0);
|
||||
const applyLayoutWhenReady = () => {
|
||||
const hpState = hangingProtocolService.getState();
|
||||
|
||||
if (!hpState) {
|
||||
console.log('### HANGING PROTOCOL PAS ENCORE PRET -> ATTENTE ###');
|
||||
|
||||
window.setTimeout(applyLayoutWhenReady, 100);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('### HANGING PROTOCOL PRET -> APPLICATION LAYOUT ###');
|
||||
|
||||
completeLayout();
|
||||
};
|
||||
|
||||
applyLayoutWhenReady();
|
||||
},
|
||||
|
||||
toggleOneUp() {
|
||||
@@ -805,9 +855,7 @@ const commandsModule = ({
|
||||
}
|
||||
const reportBlob = datasetToDicomBlob(instances[0]);
|
||||
const type = defaultContentType || 'application/dicom';
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({ [type]: reportBlob }),
|
||||
]);
|
||||
await navigator.clipboard.write([new ClipboardItem({ [type]: reportBlob })]);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { TrizRisConfig } from '@ohif/core/src/config/TrizRisConfig';
|
||||
|
||||
const ACCESS_URL = `${TrizRisConfig.apiBaseUrl}/ohif/access`;
|
||||
|
||||
export async function checkTrizRisAccess(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(ACCESS_URL, {
|
||||
method: 'GET',
|
||||
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
return result.authorized === true;
|
||||
} catch (error) {
|
||||
console.error('### ERREUR VERIFICATION ACCES TRIZRIS ###', error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { TrizRisConfig } from '@ohif/core/src/config/TrizRisConfig';
|
||||
const BASE_URL = `${TrizRisConfig.apiBaseUrl}/image-view-state/current`;
|
||||
|
||||
export type ImageViewState = {
|
||||
zoom: number;
|
||||
rotation: number;
|
||||
studyInstanceUID: string;
|
||||
seriesInstanceUID: string;
|
||||
sopInstanceUID: string;
|
||||
};
|
||||
|
||||
export async function saveImageViewState(state: ImageViewState): Promise<void> {
|
||||
const response = await fetch(BASE_URL, {
|
||||
method: 'POST',
|
||||
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
|
||||
credentials: 'include',
|
||||
|
||||
body: JSON.stringify(state),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur sauvegarde image view state : ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getImageViewState(
|
||||
studyInstanceUID: string,
|
||||
seriesInstanceUID: string,
|
||||
sopInstanceUID: string
|
||||
): Promise<ImageViewState | null> {
|
||||
const url =
|
||||
`${BASE_URL}` +
|
||||
`?studyInstanceUID=${encodeURIComponent(studyInstanceUID)}` +
|
||||
`&seriesInstanceUID=${encodeURIComponent(seriesInstanceUID)}` +
|
||||
`&sopInstanceUID=${encodeURIComponent(sopInstanceUID)}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur récupération image view state : ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { TrizRisConfig } from '@ohif/core/src/config/TrizRisConfig';
|
||||
const BASE_URL = `${TrizRisConfig.apiBaseUrl}/layout/current`;
|
||||
|
||||
export type SavedLayout = {
|
||||
numRows: number;
|
||||
numCols: number;
|
||||
};
|
||||
|
||||
export async function saveLayoutPreference(
|
||||
numRows: number,
|
||||
numCols: number,
|
||||
studyInstanceUID: string
|
||||
): Promise<void> {
|
||||
const response = await fetch(BASE_URL, {
|
||||
method: 'POST',
|
||||
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
|
||||
credentials: 'include',
|
||||
|
||||
body: JSON.stringify({
|
||||
numRows,
|
||||
numCols,
|
||||
studyInstanceUID,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur sauvegarde layout TrizRis : ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLayoutPreference(studyInstanceUID: string): Promise<SavedLayout | null> {
|
||||
const response = await fetch(
|
||||
`${BASE_URL}?studyInstanceUID=${encodeURIComponent(studyInstanceUID)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
|
||||
credentials: 'include',
|
||||
}
|
||||
);
|
||||
|
||||
if (response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur récupération layout TrizRis : ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import update from 'immutability-helper';
|
||||
import { utils } from '@ohif/core';
|
||||
|
||||
import { getLayoutPreference } from '../../../extensions/default/src/services/TrizRisLayoutApi';
|
||||
import initToolGroups from './initToolGroups';
|
||||
import { checkTrizRisAccess } from '../../../extensions/default/src/services/TrizRisAccessApi';
|
||||
import {
|
||||
addActivatePanelTriggers,
|
||||
applyToolGroupAdditions,
|
||||
@@ -182,9 +183,19 @@ export const defaultActivatePanelTriggers = [
|
||||
},
|
||||
];
|
||||
|
||||
export function onModeEnter({ servicesManager, extensionManager, commandsManager }: withAppTypes) {
|
||||
const { measurementService, toolbarService, toolGroupService, customizationService } =
|
||||
servicesManager.services;
|
||||
export function onModeEnter({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
studyInstanceUIDs,
|
||||
}: withAppTypes) {
|
||||
const {
|
||||
measurementService,
|
||||
toolbarService,
|
||||
toolGroupService,
|
||||
customizationService,
|
||||
hangingProtocolService,
|
||||
} = servicesManager.services;
|
||||
|
||||
measurementService.clearMeasurements();
|
||||
|
||||
@@ -220,7 +231,7 @@ export function onModeEnter({ servicesManager, extensionManager, commandsManager
|
||||
);
|
||||
|
||||
// Note: the mode's `modeCustomizations` are NOT applied here — the mode
|
||||
// route applies them right after the mode scope is reset, before the app
|
||||
// route applies them right after the mode scope is reset, before the appf
|
||||
// config / URL `mode` phase blocks, so the final value of every key is
|
||||
// decided purely by customization scope precedence and application order.
|
||||
|
||||
@@ -230,6 +241,47 @@ export function onModeEnter({ servicesManager, extensionManager, commandsManager
|
||||
this._unsubscriptions.push(
|
||||
...addActivatePanelTriggers({ servicesManager }, this.activatePanelTriggers)
|
||||
);
|
||||
|
||||
const restoreLayoutTrizRis = async (attempt = 0) => {
|
||||
try {
|
||||
const studyInstanceUID = studyInstanceUIDs?.[0];
|
||||
|
||||
if (!studyInstanceUID) {
|
||||
console.log('### STUDY UID PAS ENCORE DISPONIBLE ###');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('### STUDY UID DIRECT POUR LAYOUT ###', studyInstanceUID);
|
||||
console.log('### STUDY UID POUR LAYOUT ###', studyInstanceUID);
|
||||
|
||||
const layout = await getLayoutPreference(studyInstanceUID);
|
||||
|
||||
console.log('### LAYOUT RECUPERE DEPUIS TRIZRIS ###', layout);
|
||||
|
||||
if (!layout) {
|
||||
console.log('### AUCUNE PREFERENCE LAYOUT -> OHIF / HANGING PROTOCOL CONSERVE ###');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('### PREFERENCE LAYOUT TROUVEE -> PRIORITE TRIZRIS ###');
|
||||
|
||||
commandsManager.runCommand('setViewportGridLayout', {
|
||||
numRows: layout.numRows,
|
||||
numCols: layout.numCols,
|
||||
isHangingProtocolLayout: false,
|
||||
savePreference: false,
|
||||
});
|
||||
|
||||
console.log('### LAYOUT TRIZRIS APPLIQUE ###', layout.numRows, 'x', layout.numCols);
|
||||
|
||||
console.log('### LAYOUT TRIZRIS APPLIQUE ###', layout.numRows, 'x', layout.numCols);
|
||||
} catch (error) {
|
||||
console.error('### ERREUR RESTAURATION LAYOUT TRIZRIS ###', error);
|
||||
}
|
||||
};
|
||||
|
||||
restoreLayoutTrizRis();
|
||||
}
|
||||
|
||||
export function onModeExit({ servicesManager }: withAppTypes) {
|
||||
@@ -240,6 +292,7 @@ export function onModeExit({ servicesManager }: withAppTypes) {
|
||||
cornerstoneViewportService,
|
||||
uiDialogService,
|
||||
uiModalService,
|
||||
hangingProtocolService,
|
||||
} = servicesManager.services;
|
||||
|
||||
this._unsubscriptions?.forEach(unsubscribe => unsubscribe());
|
||||
|
||||
@@ -83,11 +83,15 @@ function initDefaultToolGroup(extensionManager, toolGroupService, commandsManage
|
||||
{ toolName: toolNames.LivewireContour },
|
||||
{ toolName: toolNames.WindowLevelRegion },
|
||||
],
|
||||
enabled: [{ toolName: toolNames.ImageOverlayViewer }, { toolName: toolNames.ReferenceLines }],
|
||||
enabled: [{ toolName: toolNames.ImageOverlayViewer }],
|
||||
|
||||
disabled: [
|
||||
{
|
||||
toolName: toolNames.AdvancedMagnify,
|
||||
},
|
||||
{
|
||||
toolName: toolNames.ReferenceLines,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -209,6 +209,15 @@ module.exports = (env, argv) => {
|
||||
context: ['/dicomweb'],
|
||||
target: 'http://localhost:5000',
|
||||
},
|
||||
|
||||
{
|
||||
context: ['/orthanc'],
|
||||
target: 'http://localhost:8042',
|
||||
changeOrigin: true,
|
||||
pathRewrite: {
|
||||
'^/orthanc': '',
|
||||
},
|
||||
},
|
||||
],
|
||||
static: [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
TRIZRIS_BASE_URL: 'http://localhost:9090/TrizRis',
|
||||
ORTHANC_BASE_URL: 'http://localhost:8042',
|
||||
OHIF_BASE_URL: 'http://localhost:3000',
|
||||
};
|
||||
@@ -16,12 +16,18 @@
|
||||
// your own DICOMweb server. For a fully-featured setup with every data source
|
||||
// and customization loading enabled, see config/dev.js (local development) and
|
||||
// config/netlify.js (the public demo deploy).
|
||||
|
||||
window.config = {
|
||||
name: 'config/default.js',
|
||||
|
||||
routerBasename: null,
|
||||
|
||||
// whiteLabeling: {},
|
||||
|
||||
extensions: [],
|
||||
|
||||
modes: [],
|
||||
|
||||
customizationService: {},
|
||||
|
||||
// --- URL-driven customizations (?customization=) ----------------------------
|
||||
@@ -44,68 +50,112 @@ window.config = {
|
||||
// ("next") API instead of the legacy Stack/Volume viewport classes.
|
||||
genericViewports: {
|
||||
enabled: false,
|
||||
|
||||
// Render backend selection: 'cpu' | 'webgl' | 'auto' | a backend id
|
||||
// registered via cornerstone's registerRenderBackend (e.g. a webgpu
|
||||
// backend), or a map with per-viewport-type overrides, e.g.
|
||||
// { default: 'webgl', orthographic: 'cpu' }. The matching URL params take
|
||||
// precedence per-session: ?viewportRendering=cpu and
|
||||
// ?orthographic.viewportRendering=cpu.
|
||||
|
||||
// viewportRendering: 'auto',
|
||||
},
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
showStudyList: true,
|
||||
|
||||
// some windows systems have issues with more than 3 web workers
|
||||
maxNumberOfWebWorkers: 3,
|
||||
|
||||
// below flag is for performance reasons, but it might not work for all servers
|
||||
showWarningMessageForCrossOrigin: true,
|
||||
|
||||
showCPUFallbackMessage: true,
|
||||
|
||||
showLoadingIndicator: true,
|
||||
|
||||
experimentalStudyBrowserSort: false,
|
||||
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
|
||||
groupEnabledModesFirst: true,
|
||||
|
||||
allowMultiSelectExport: false,
|
||||
|
||||
maxNumRequests: {
|
||||
interaction: 100,
|
||||
thumbnail: 5,
|
||||
|
||||
// Prefetch number is dependent on the http protocol. For http 2 or
|
||||
// above, the number of requests can be go a lot higher.
|
||||
prefetch: 25,
|
||||
},
|
||||
showErrorDetails: 'always', // 'always', 'dev', 'production'
|
||||
|
||||
showErrorDetails: 'always',
|
||||
|
||||
// `dangerouslyUseDynamicConfig` (load configuration from a `configUrl` query
|
||||
// parameter) is intentionally left OFF in the secure default build. See
|
||||
// config/dev.js for the documented shape.
|
||||
defaultDataSourceName: 'ohif',
|
||||
|
||||
// ✅ MODIFIÉ : on indique que la source par défaut est Orthanc
|
||||
defaultDataSourceName: 'orthanc',
|
||||
|
||||
dataSources: [
|
||||
{
|
||||
// Read-only public demo server. Replace with your own DICOMweb server.
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'ohif',
|
||||
|
||||
// ✅ MODIFIÉ : nom logique de la datasource
|
||||
sourceName: 'orthanc',
|
||||
|
||||
configuration: {
|
||||
friendlyName: 'AWS S3 Static wado server',
|
||||
name: 'aws',
|
||||
wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb',
|
||||
qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb',
|
||||
wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
// ✅ MODIFIÉ
|
||||
friendlyName: 'Orthanc local',
|
||||
|
||||
// ✅ MODIFIÉ
|
||||
name: 'Orthanc',
|
||||
|
||||
// ✅ Orthanc WADO-URI
|
||||
wadoUriRoot: '/orthanc/wado',
|
||||
|
||||
// ✅ Orthanc QIDO-RS
|
||||
qidoRoot: '/orthanc/dicom-web',
|
||||
|
||||
// ✅ Orthanc WADO-RS
|
||||
wadoRoot: '/orthanc/dicom-web',
|
||||
|
||||
qidoSupportsIncludeField: true,
|
||||
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'thumbnail',
|
||||
|
||||
thumbnailRendering: 'wadors',
|
||||
|
||||
thumbnailRequestStrategy: 'fetch',
|
||||
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: true,
|
||||
|
||||
supportsFuzzyMatching: false,
|
||||
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
|
||||
// ✅ MODIFIÉ
|
||||
// Avant tu avais true car la config venait d'un serveur WADO statique.
|
||||
// Avec Orthanc DICOMweb, mets false.
|
||||
staticWado: false,
|
||||
|
||||
// Multiframe SEG loads fetch the whole instance as a single Part 10
|
||||
// object by default and wait for it: the per-frame endpoint is
|
||||
// efficient, but SEG frames are so small and numerous that one bulk
|
||||
// fetch beats hundreds of tiny requests. Per-frame loading is the
|
||||
// exception — set loadMultiframeAsPart10: false here to force it.
|
||||
// fetch beats hundreds of tiny requests.
|
||||
singlepart: 'bulkdata,video',
|
||||
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
|
||||
transform: url => url.replace('/pixeldata.mp4', '/rendered'),
|
||||
},
|
||||
|
||||
omitQuotationForMultipartRequest: true,
|
||||
},
|
||||
},
|
||||
@@ -114,15 +164,13 @@ window.config = {
|
||||
// default because they broaden the attack surface of a default deployment.
|
||||
// Enable them only in a deployment you control (see config/dev.js):
|
||||
// - dicomlocal: loads DICOM files from the user's machine.
|
||||
// - dicomjson: loads metadata from an arbitrary `?url=` (gate with
|
||||
// `dangerouslyAllowedOriginsForAuthenticatedEnvironments`).
|
||||
// - dicomjson: loads metadata from an arbitrary `?url=`.
|
||||
// - dicomwebproxy: delegating proxy driven by `?url=`.
|
||||
],
|
||||
|
||||
httpErrorHandler: error => {
|
||||
// This is 429 when rejected from the public idc sandbox too often.
|
||||
console.warn(error.status);
|
||||
|
||||
// Could use services manager here to bring up a dialog/modal if needed.
|
||||
console.warn('test, navigate to https://ohif.org/');
|
||||
},
|
||||
};
|
||||
@@ -10,52 +10,64 @@
|
||||
// customizations, can be exercised locally exactly as it runs on the demo.
|
||||
// The locked-down config/default.js is what a plain production build emits
|
||||
// instead.
|
||||
|
||||
window.config = {
|
||||
name: 'config/dev.js',
|
||||
|
||||
routerBasename: null,
|
||||
|
||||
// whiteLabeling: {},
|
||||
|
||||
extensions: [],
|
||||
|
||||
modes: [],
|
||||
|
||||
customizationService: ['@ohif/extension-default.customizationModule.theme'],
|
||||
|
||||
// URL-driven customizations (?customization=). The `default` prefix (no
|
||||
// slashes) is used for values without a leading slash; every other prefix
|
||||
// must start AND end with a slash and matches the leading `/segment/` of the
|
||||
// value. Files are fetched and parsed as JSONC data — never executed.
|
||||
// e.g. ?customization=tools/ctPresets -> ./customizations/tools/ctPresets.jsonc
|
||||
customizationUrlPrefixes: {
|
||||
default: './customizations/',
|
||||
},
|
||||
|
||||
showStudyList: true,
|
||||
// some windows systems have issues with more than 3 web workers
|
||||
|
||||
maxNumberOfWebWorkers: 3,
|
||||
// below flag is for performance reasons, but it might not work for all servers
|
||||
|
||||
showWarningMessageForCrossOrigin: true,
|
||||
|
||||
showCPUFallbackMessage: true,
|
||||
|
||||
showLoadingIndicator: true,
|
||||
|
||||
experimentalStudyBrowserSort: false,
|
||||
|
||||
strictZSpacingForVolumeViewport: true,
|
||||
|
||||
groupEnabledModesFirst: true,
|
||||
|
||||
allowMultiSelectExport: false,
|
||||
|
||||
maxNumRequests: {
|
||||
interaction: 100,
|
||||
thumbnail: 5,
|
||||
// Prefetch number is dependent on the http protocol. For http 2 or
|
||||
// above, the number of requests can be go a lot higher.
|
||||
prefetch: 25,
|
||||
},
|
||||
showErrorDetails: 'always', // 'always', 'dev', 'production'
|
||||
|
||||
showErrorDetails: 'always',
|
||||
|
||||
// filterQueryParam: false,
|
||||
// Defines multi-monitor layouts
|
||||
|
||||
multimonitor: [
|
||||
{
|
||||
id: 'split',
|
||||
|
||||
test: ({ multimonitor }) => multimonitor === 'split',
|
||||
|
||||
screens: [
|
||||
{
|
||||
id: 'ohif0',
|
||||
|
||||
screen: null,
|
||||
|
||||
location: {
|
||||
screen: 0,
|
||||
width: 0.5,
|
||||
@@ -63,17 +75,22 @@ window.config = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
|
||||
options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'ohif1',
|
||||
|
||||
screen: null,
|
||||
|
||||
location: {
|
||||
width: 0.5,
|
||||
height: 1,
|
||||
left: 0.5,
|
||||
top: 0,
|
||||
},
|
||||
|
||||
options: 'location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||
},
|
||||
],
|
||||
@@ -81,195 +98,362 @@ window.config = {
|
||||
|
||||
{
|
||||
id: '2',
|
||||
|
||||
test: ({ multimonitor }) => multimonitor === '2',
|
||||
|
||||
screens: [
|
||||
{
|
||||
id: 'ohif0',
|
||||
|
||||
screen: 0,
|
||||
|
||||
location: {
|
||||
width: 1,
|
||||
height: 1,
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
|
||||
options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||
},
|
||||
|
||||
{
|
||||
id: 'ohif1',
|
||||
|
||||
screen: 1,
|
||||
|
||||
location: {
|
||||
width: 1,
|
||||
height: 1,
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
|
||||
options: 'fullscreen=yes,location=no,menubar=no,scrollbars=no,status=no,titlebar=no',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
defaultDataSourceName: 'ohif',
|
||||
/* Dynamic config allows user to pass "configUrl" query string this allows to load config without recompiling application. The regex will ensure valid configuration source */
|
||||
// dangerouslyUseDynamicConfig: {
|
||||
// enabled: true,
|
||||
// regex: /.*/,
|
||||
// },
|
||||
|
||||
// ✅ MODIFIÉ
|
||||
// Avant :
|
||||
// defaultDataSourceName: 'ohif',
|
||||
//
|
||||
// Maintenant OHIF utilise Orthanc par défaut.
|
||||
defaultDataSourceName: 'orthanc',
|
||||
|
||||
dataSources: [
|
||||
// =========================================================
|
||||
// AWS source 1
|
||||
// =========================================================
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
|
||||
sourceName: 'ohif',
|
||||
|
||||
configuration: {
|
||||
friendlyName: 'AWS S3 Static wado server',
|
||||
|
||||
name: 'aws',
|
||||
|
||||
wadoUriRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb',
|
||||
|
||||
qidoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb',
|
||||
|
||||
wadoRoot: 'https://d14fa38qiwhyfd.cloudfront.net/dicomweb',
|
||||
|
||||
qidoSupportsIncludeField: false,
|
||||
|
||||
imageRendering: 'wadors',
|
||||
|
||||
thumbnailRendering: 'thumbnail',
|
||||
|
||||
thumbnailRequestStrategy: 'fetch',
|
||||
|
||||
enableStudyLazyLoad: true,
|
||||
|
||||
supportsFuzzyMatching: true,
|
||||
|
||||
supportsWildcard: true,
|
||||
|
||||
staticWado: true,
|
||||
|
||||
singlepart: 'bulkdata,video',
|
||||
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
|
||||
relativeResolution: 'studies',
|
||||
|
||||
transform: url => url.replace('/pixeldata.mp4', '/rendered'),
|
||||
},
|
||||
|
||||
omitQuotationForMultipartRequest: true,
|
||||
},
|
||||
},
|
||||
|
||||
// =========================================================
|
||||
// AWS source 2
|
||||
// =========================================================
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
|
||||
sourceName: 'ohif2',
|
||||
|
||||
configuration: {
|
||||
friendlyName: 'AWS S3 Static wado secondary server',
|
||||
|
||||
name: 'aws',
|
||||
|
||||
wadoUriRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb',
|
||||
|
||||
qidoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb',
|
||||
|
||||
wadoRoot: 'https://dd14fa38qiwhyfd.cloudfront.net/dicomweb',
|
||||
|
||||
qidoSupportsIncludeField: false,
|
||||
|
||||
supportsReject: false,
|
||||
|
||||
imageRendering: 'wadors',
|
||||
|
||||
thumbnailRendering: 'wadors',
|
||||
|
||||
enableStudyLazyLoad: true,
|
||||
|
||||
supportsFuzzyMatching: false,
|
||||
|
||||
supportsWildcard: true,
|
||||
|
||||
staticWado: true,
|
||||
|
||||
singlepart: 'bulkdata,video',
|
||||
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
omitQuotationForMultipartRequest: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'ohif3',
|
||||
configuration: {
|
||||
friendlyName: 'AWS S3 Static wado secondary server',
|
||||
name: 'aws',
|
||||
wadoUriRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb',
|
||||
qidoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb',
|
||||
wadoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb',
|
||||
qidoSupportsIncludeField: false,
|
||||
supportsReject: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: false,
|
||||
supportsWildcard: true,
|
||||
staticWado: true,
|
||||
singlepart: 'bulkdata,video',
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
|
||||
omitQuotationForMultipartRequest: true,
|
||||
},
|
||||
},
|
||||
|
||||
// =========================================================
|
||||
// AWS source 3
|
||||
// =========================================================
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
|
||||
sourceName: 'ohif3',
|
||||
|
||||
configuration: {
|
||||
friendlyName: 'AWS S3 Static wado secondary server',
|
||||
|
||||
name: 'aws',
|
||||
|
||||
wadoUriRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb',
|
||||
|
||||
qidoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb',
|
||||
|
||||
wadoRoot: 'https://d3t6nz73ql33tx.cloudfront.net/dicomweb',
|
||||
|
||||
qidoSupportsIncludeField: false,
|
||||
|
||||
supportsReject: false,
|
||||
|
||||
imageRendering: 'wadors',
|
||||
|
||||
thumbnailRendering: 'wadors',
|
||||
|
||||
enableStudyLazyLoad: true,
|
||||
|
||||
supportsFuzzyMatching: false,
|
||||
|
||||
supportsWildcard: true,
|
||||
|
||||
staticWado: true,
|
||||
|
||||
singlepart: 'bulkdata,video',
|
||||
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
|
||||
omitQuotationForMultipartRequest: true,
|
||||
},
|
||||
},
|
||||
|
||||
// =========================================================
|
||||
// local5000
|
||||
// =========================================================
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
|
||||
sourceName: 'local5000',
|
||||
|
||||
configuration: {
|
||||
friendlyName: 'Static WADO Local Data',
|
||||
|
||||
name: 'DCM4CHEE',
|
||||
|
||||
qidoRoot: 'http://localhost:5000/dicomweb',
|
||||
|
||||
wadoRoot: 'http://localhost:5000/dicomweb',
|
||||
|
||||
qidoSupportsIncludeField: false,
|
||||
|
||||
supportsReject: true,
|
||||
|
||||
supportsStow: true,
|
||||
|
||||
imageRendering: 'wadors',
|
||||
|
||||
thumbnailRendering: 'wadors',
|
||||
|
||||
enableStudyLazyLoad: true,
|
||||
|
||||
supportsFuzzyMatching: false,
|
||||
|
||||
supportsWildcard: true,
|
||||
|
||||
staticWado: true,
|
||||
|
||||
singlepart: 'video',
|
||||
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// =========================================================
|
||||
// ✅ ORTHANC LOCAL VIA PROXY OHIF
|
||||
// =========================================================
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomweb',
|
||||
|
||||
sourceName: 'orthanc',
|
||||
|
||||
configuration: {
|
||||
friendlyName: 'local Orthanc DICOMWeb Server',
|
||||
name: 'DCM4CHEE',
|
||||
wadoUriRoot: 'http://localhost/pacs/dicom-web',
|
||||
qidoRoot: 'http://localhost/pacs/dicom-web',
|
||||
wadoRoot: 'http://localhost/pacs/dicom-web',
|
||||
// ✅ MODIFIÉ
|
||||
friendlyName: 'Orthanc local',
|
||||
|
||||
// ✅ MODIFIÉ
|
||||
name: 'Orthanc',
|
||||
|
||||
/*
|
||||
* ✅ MODIFIÉ IMPORTANT
|
||||
*
|
||||
* AVANT :
|
||||
* http://localhost:8042/wado
|
||||
*
|
||||
* MAINTENANT :
|
||||
* on passe par le serveur OHIF lui-même.
|
||||
*/
|
||||
wadoUriRoot: '/orthanc/wado',
|
||||
|
||||
/*
|
||||
* ✅ MODIFIÉ IMPORTANT
|
||||
*
|
||||
* AVANT :
|
||||
* http://localhost:8042/dicom-web
|
||||
*/
|
||||
qidoRoot: '/orthanc/dicom-web',
|
||||
|
||||
/*
|
||||
* ✅ MODIFIÉ IMPORTANT
|
||||
*
|
||||
* AVANT :
|
||||
* http://localhost:8042/dicom-web
|
||||
*/
|
||||
wadoRoot: '/orthanc/dicom-web',
|
||||
|
||||
qidoSupportsIncludeField: true,
|
||||
|
||||
supportsReject: true,
|
||||
|
||||
dicomUploadEnabled: true,
|
||||
|
||||
imageRendering: 'wadors',
|
||||
|
||||
thumbnailRendering: 'wadors',
|
||||
|
||||
// ✅ AJOUTÉ
|
||||
thumbnailRequestStrategy: 'fetch',
|
||||
|
||||
enableStudyLazyLoad: true,
|
||||
supportsFuzzyMatching: true,
|
||||
|
||||
// ✅ MODIFIÉ
|
||||
supportsFuzzyMatching: false,
|
||||
|
||||
supportsWildcard: true,
|
||||
|
||||
// ✅ AJOUTÉ
|
||||
staticWado: false,
|
||||
|
||||
// ✅ AJOUTÉ
|
||||
singlepart: 'bulkdata,video',
|
||||
|
||||
omitQuotationForMultipartRequest: true,
|
||||
|
||||
bulkDataURI: {
|
||||
enabled: true,
|
||||
|
||||
// ✅ AJOUTÉ
|
||||
relativeResolution: 'studies',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// =========================================================
|
||||
// dicomweb proxy
|
||||
// =========================================================
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomwebproxy',
|
||||
|
||||
sourceName: 'dicomwebproxy',
|
||||
|
||||
configuration: {
|
||||
friendlyName: 'dicomweb delegating proxy',
|
||||
|
||||
name: 'dicomwebproxy',
|
||||
},
|
||||
},
|
||||
|
||||
// =========================================================
|
||||
// dicomjson
|
||||
// =========================================================
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomjson',
|
||||
|
||||
sourceName: 'dicomjson',
|
||||
|
||||
configuration: {
|
||||
friendlyName: 'dicom json',
|
||||
|
||||
name: 'json',
|
||||
},
|
||||
},
|
||||
|
||||
// =========================================================
|
||||
// dicomlocal
|
||||
// =========================================================
|
||||
{
|
||||
namespace: '@ohif/extension-default.dataSourcesModule.dicomlocal',
|
||||
|
||||
sourceName: 'dicomlocal',
|
||||
|
||||
configuration: {
|
||||
friendlyName: 'dicom local',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
httpErrorHandler: error => {
|
||||
// This is 429 when rejected from the public idc sandbox too often.
|
||||
console.warn(error.status);
|
||||
|
||||
// Could use services manager here to bring up a dialog/modal if needed.
|
||||
console.warn('test, navigate to https://ohif.org/');
|
||||
},
|
||||
};
|
||||
@@ -317,6 +317,8 @@ export default function ModeRoute({
|
||||
extensionManager,
|
||||
commandsManager,
|
||||
appConfig,
|
||||
// ===== TRIZRIS =====
|
||||
studyInstanceUIDs,
|
||||
});
|
||||
|
||||
// Move hotkeys setup here, after onModeEnter
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function NoAccess() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
background: '#0b0f14',
|
||||
color: 'white',
|
||||
textAlign: 'center',
|
||||
padding: '20px',
|
||||
}}
|
||||
>
|
||||
<h1>Accès refusé</h1>
|
||||
|
||||
<p>Vous devez être connecté à TrizRis avec un compte médecin pour accéder au viewer.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { checkTrizRisAccess } from '../../../../extensions/default/src/services/TrizRisAccessApi';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function TrizRisProtectedRoute({ children }: Props) {
|
||||
const [checking, setChecking] = useState(true);
|
||||
const [authorized, setAuthorized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const checkAccess = async () => {
|
||||
try {
|
||||
const result = await checkTrizRisAccess();
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('### VERIFICATION ACCES TRIZRIS ###', result);
|
||||
|
||||
setAuthorized(result);
|
||||
} catch (error) {
|
||||
console.error('### ERREUR VERIFICATION ACCES TRIZRIS ###', error);
|
||||
|
||||
if (mounted) {
|
||||
setAuthorized(false);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setChecking(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkAccess();
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Pendant la vérification :
|
||||
// on n'affiche PAS encore le viewer.
|
||||
if (checking) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
background: '#0b0f14',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!authorized) {
|
||||
return (
|
||||
<Navigate
|
||||
to="/no-access"
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import PropTypes from 'prop-types';
|
||||
import { routerBasename } from '../utils/publicUrl';
|
||||
import { useAppConfig } from '@state';
|
||||
import { history } from '../utils/history';
|
||||
import TrizRisProtectedRoute from './TrizRisProtectedRoute';
|
||||
import NoAccess from './NoAccess';
|
||||
|
||||
const NotFoundServer = ({
|
||||
message = 'Unable to query for studies at this time. Check your data source configuration or network connection',
|
||||
@@ -71,6 +73,10 @@ NotFoundStudy.propTypes = {
|
||||
|
||||
// TODO: Include "routes" debug route if dev build
|
||||
const bakedInRoutes = [
|
||||
{
|
||||
path: `/no-access`,
|
||||
children: NoAccess,
|
||||
},
|
||||
{
|
||||
path: `/notfoundserver`,
|
||||
children: NotFoundServer,
|
||||
@@ -105,7 +111,7 @@ const createRoutes = ({
|
||||
hotkeysManager,
|
||||
showStudyList,
|
||||
}: withAppTypes) => {
|
||||
const routes =
|
||||
const routes = (
|
||||
buildModeRoutes({
|
||||
modes,
|
||||
dataSources,
|
||||
@@ -113,7 +119,11 @@ const createRoutes = ({
|
||||
servicesManager,
|
||||
commandsManager,
|
||||
hotkeysManager,
|
||||
}) || [];
|
||||
}) || []
|
||||
).map(route => ({
|
||||
...route,
|
||||
trizRisProtected: true,
|
||||
}));
|
||||
|
||||
const { customizationService } = servicesManager.services;
|
||||
|
||||
@@ -176,23 +186,27 @@ const createRoutes = ({
|
||||
return (
|
||||
<Routes>
|
||||
{allRoutes.map((route, i) => {
|
||||
return route.private === true ? (
|
||||
let element = <RouteWithErrorBoundary route={route} />;
|
||||
|
||||
if (route.trizRisProtected === true) {
|
||||
element = <TrizRisProtectedRoute>{element}</TrizRisProtectedRoute>;
|
||||
}
|
||||
|
||||
if (route.private === true) {
|
||||
element = (
|
||||
<PrivateRoute
|
||||
handleUnauthenticated={() => userAuthenticationService.handleUnauthenticated()}
|
||||
>
|
||||
{element}
|
||||
</PrivateRoute>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Route
|
||||
key={i}
|
||||
path={route.path}
|
||||
element={
|
||||
<PrivateRoute
|
||||
handleUnauthenticated={() => userAuthenticationService.handleUnauthenticated()}
|
||||
>
|
||||
<RouteWithErrorBoundary route={route} />
|
||||
</PrivateRoute>
|
||||
}
|
||||
></Route>
|
||||
) : (
|
||||
<Route
|
||||
key={i}
|
||||
path={route.path}
|
||||
element={<RouteWithErrorBoundary route={route} />}
|
||||
element={element}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
const TRIZRIS_BASE_URL = 'http://localhost:9090/TrizRis';
|
||||
const ORTHANC_BASE_URL = 'http://localhost:8042';
|
||||
|
||||
export const TrizRisConfig = {
|
||||
apiBaseUrl: `${TRIZRIS_BASE_URL}/api`,
|
||||
accessUrl: `${TRIZRIS_BASE_URL}/ohif/access`,
|
||||
|
||||
orthancBaseUrl: ORTHANC_BASE_URL,
|
||||
orthancDicomWebUrl: `${ORTHANC_BASE_URL}/dicom-web`,
|
||||
orthancWadoUrl: `${ORTHANC_BASE_URL}/wado`,
|
||||
|
||||
ohifBaseUrl: 'http://localhost:3000',
|
||||
};
|
||||
+17
-3
@@ -1,5 +1,6 @@
|
||||
import { annotation } from '@cornerstonejs/tools';
|
||||
import LocalStorageAnnotationPersistenceBackend from './LocalStorageAnnotationPersistenceBackend';
|
||||
import TrizRisAnnotationPersistenceBackend from './TrizRisAnnotationPersistenceBackend';
|
||||
|
||||
export interface AnnotationPersistenceBackend {
|
||||
save(record: Record<string, any>): Promise<void> | void;
|
||||
@@ -28,7 +29,8 @@ export default class AnnotationPersistenceService {
|
||||
name: 'annotationPersistenceService',
|
||||
altName: 'AnnotationPersistenceService',
|
||||
create: ({ servicesManager }: { servicesManager: any }) => {
|
||||
const backend = new LocalStorageAnnotationPersistenceBackend();
|
||||
// = new LocalStorageAnnotationPersistenceBackend();
|
||||
const backend = new TrizRisAnnotationPersistenceBackend();
|
||||
return new AnnotationPersistenceService(backend, {
|
||||
services: {
|
||||
measurementService: servicesManager.services.measurementService,
|
||||
@@ -207,8 +209,7 @@ export default class AnnotationPersistenceService {
|
||||
const resolvedSource =
|
||||
(persistedSource?.name && persistedSource?.version
|
||||
? measurementService.getSource(persistedSource.name, persistedSource.version)
|
||||
: null) ||
|
||||
measurementService.getSource('Cornerstone3DTools', '0.1');
|
||||
: null) || measurementService.getSource('Cornerstone3DTools', '0.1');
|
||||
|
||||
if (!resolvedSource) {
|
||||
console.warn('Annotation persistence: no registered source found, skipping restore');
|
||||
@@ -232,7 +233,20 @@ export default class AnnotationPersistenceService {
|
||||
|
||||
const normalizedAnnotation = this._normalizePersistedAnnotation(record.annotation);
|
||||
const rebuiltAnnotation = this._buildRestoredAnnotation(measurement, normalizedAnnotation);
|
||||
console.log('====================================');
|
||||
console.log('### RESTAURATION MEASUREMENT ###');
|
||||
|
||||
console.log('referencedImageId =', rebuiltAnnotation?.metadata?.referencedImageId);
|
||||
|
||||
console.log('FrameOfReferenceUID =', rebuiltAnnotation?.metadata?.FrameOfReferenceUID);
|
||||
|
||||
console.log('points JSON =', JSON.stringify(rebuiltAnnotation?.data?.handles?.points));
|
||||
|
||||
console.log('cachedStats JSON =', JSON.stringify(rebuiltAnnotation?.data?.cachedStats));
|
||||
|
||||
console.log('measurement JSON =', JSON.stringify(measurement));
|
||||
|
||||
console.log('====================================');
|
||||
measurementService.addOrUpdateMeasurement?.(measurement, {
|
||||
annotation: rebuiltAnnotation,
|
||||
});
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
export interface AnnotationPersistenceBackend {
|
||||
save(record: Record<string, any>): Promise<void> | void;
|
||||
update(record: Record<string, any>): Promise<void> | void;
|
||||
delete(uid: string): Promise<void> | void;
|
||||
load(studyInstanceUID: string): Promise<any[]> | any[];
|
||||
}
|
||||
|
||||
const BASE_URL = 'http://localhost:9090/TrizRis/api/annotation';
|
||||
|
||||
export default class TrizRisAnnotationPersistenceBackend implements AnnotationPersistenceBackend {
|
||||
public async save(record: Record<string, any>): Promise<void> {
|
||||
console.log('### SAUVEGARDE ANNOTATION TRIZRIS ###');
|
||||
|
||||
console.log(record);
|
||||
|
||||
const response = await fetch(BASE_URL, {
|
||||
method: 'POST',
|
||||
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
body: JSON.stringify(record),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur sauvegarde annotation : ${response.status}`);
|
||||
}
|
||||
|
||||
const resultat = await response.text();
|
||||
|
||||
console.log('Réponse TrizRis =', resultat);
|
||||
}
|
||||
|
||||
public async update(record: Record<string, any>): Promise<void> {
|
||||
console.log('### MODIFICATION ANNOTATION TRIZRIS ###');
|
||||
|
||||
console.log(record);
|
||||
|
||||
const response = await fetch(BASE_URL, {
|
||||
method: 'PUT',
|
||||
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
body: JSON.stringify(record),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur modification annotation : ${response.status}`);
|
||||
}
|
||||
|
||||
const resultat = await response.text();
|
||||
|
||||
console.log('Réponse TrizRis =', resultat);
|
||||
}
|
||||
|
||||
public async delete(uid: string): Promise<void> {
|
||||
console.log('### SUPPRESSION ANNOTATION TRIZRIS ###');
|
||||
|
||||
console.log('UID =', uid);
|
||||
|
||||
const response = await fetch(`${BASE_URL}/${encodeURIComponent(uid)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur suppression annotation : ${response.status}`);
|
||||
}
|
||||
|
||||
const resultat = await response.text();
|
||||
|
||||
console.log('Réponse TrizRis =', resultat);
|
||||
}
|
||||
|
||||
public async load(studyInstanceUID: string): Promise<any[]> {
|
||||
console.log('### CHARGEMENT ANNOTATIONS TRIZRIS ###');
|
||||
|
||||
console.log('StudyInstanceUID =', studyInstanceUID);
|
||||
|
||||
const response = await fetch(
|
||||
`${BASE_URL}?studyInstanceUID=${encodeURIComponent(studyInstanceUID)}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Erreur chargement annotations : ${response.status}`);
|
||||
}
|
||||
|
||||
const records = await response.json();
|
||||
|
||||
console.log('Annotations reçues =', records);
|
||||
|
||||
return Array.isArray(records) ? records : [];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user