fix: retrieveSeriesMetadata (#2703)
This commit is contained in:
parent
0e2c31939c
commit
509c41fbab
@ -134,17 +134,7 @@ function createDicomJSONApi(dicomJsonConfig) {
|
|||||||
},
|
},
|
||||||
retrieve: {
|
retrieve: {
|
||||||
series: {
|
series: {
|
||||||
metaData: () => {
|
metaData: ({
|
||||||
console.debug(' DICOMJson retrieve series metadata');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
store: {
|
|
||||||
dicom: () => {
|
|
||||||
console.debug(' DICOMJson store dicom');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
retrieveSeriesMetadata: ({
|
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
madeInClient = false,
|
madeInClient = false,
|
||||||
customSort,
|
customSort,
|
||||||
@ -178,7 +168,10 @@ function createDicomJSONApi(dicomJsonConfig) {
|
|||||||
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);
|
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
|
DicomMetadataStore.addSeriesMetadata(
|
||||||
|
seriesSummaryMetadata,
|
||||||
|
madeInClient
|
||||||
|
);
|
||||||
|
|
||||||
function setSuccessFlag() {
|
function setSuccessFlag() {
|
||||||
const study = DicomMetadataStore.getStudy(
|
const study = DicomMetadataStore.getStudy(
|
||||||
@ -204,6 +197,13 @@ function createDicomJSONApi(dicomJsonConfig) {
|
|||||||
if (index === numberOfSeries - 1) setSuccessFlag();
|
if (index === numberOfSeries - 1) setSuccessFlag();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
store: {
|
||||||
|
dicom: () => {
|
||||||
|
console.debug(' DICOMJson store dicom');
|
||||||
|
},
|
||||||
|
},
|
||||||
getImageIdsForDisplaySet(displaySet) {
|
getImageIdsForDisplaySet(displaySet) {
|
||||||
const images = displaySet.images;
|
const images = displaySet.images;
|
||||||
const imageIds = [];
|
const imageIds = [];
|
||||||
|
|||||||
@ -1,69 +1,67 @@
|
|||||||
import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core'
|
import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core';
|
||||||
import OHIF from '@ohif/core'
|
import OHIF from '@ohif/core';
|
||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
|
|
||||||
const metadataProvider = OHIF.classes.MetadataProvider
|
const metadataProvider = OHIF.classes.MetadataProvider;
|
||||||
const { EVENTS } = DicomMetadataStore
|
const { EVENTS } = DicomMetadataStore;
|
||||||
|
|
||||||
// Sorting SR modalities to be at the end of series list
|
// Sorting SR modalities to be at the end of series list
|
||||||
function customSort(seriesA, seriesB) {
|
function customSort(seriesA, seriesB) {
|
||||||
const modalityA = seriesA.instances[0].Modality
|
const modalityA = seriesA.instances[0].Modality;
|
||||||
const modalityB = seriesB.instances[0].Modality
|
const modalityB = seriesB.instances[0].Modality;
|
||||||
|
|
||||||
if (modalityA === "SR") {
|
if (modalityA === 'SR') {
|
||||||
return +1;
|
return +1;
|
||||||
}
|
}
|
||||||
if (modalityB === "SR") {
|
if (modalityB === 'SR') {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function createDicomLocalApi(dicomLocalConfig) {
|
function createDicomLocalApi(dicomLocalConfig) {
|
||||||
const { name } = dicomLocalConfig
|
const { name } = dicomLocalConfig;
|
||||||
|
|
||||||
const implementation = {
|
const implementation = {
|
||||||
initialize: ({ params, query }) => {
|
initialize: ({ params, query }) => {
|
||||||
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params
|
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = params;
|
||||||
const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs')
|
const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs');
|
||||||
|
|
||||||
const StudyInstanceUIDs =
|
const StudyInstanceUIDs =
|
||||||
queryStudyInstanceUIDs || paramsStudyInstanceUIDs
|
queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
|
||||||
const StudyInstanceUIDsAsArray =
|
const StudyInstanceUIDsAsArray =
|
||||||
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
|
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
|
||||||
? StudyInstanceUIDs
|
? StudyInstanceUIDs
|
||||||
: [StudyInstanceUIDs]
|
: [StudyInstanceUIDs];
|
||||||
|
|
||||||
|
|
||||||
// Put SRs at the end of series list to make sure images are loaded first
|
// Put SRs at the end of series list to make sure images are loaded first
|
||||||
StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => {
|
StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => {
|
||||||
const study = DicomMetadataStore.getStudy(StudyInstanceUID)
|
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||||
study.series = study.series.sort(customSort)
|
study.series = study.series.sort(customSort);
|
||||||
})
|
});
|
||||||
|
|
||||||
return StudyInstanceUIDsAsArray
|
return StudyInstanceUIDsAsArray;
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
studies: {
|
studies: {
|
||||||
mapParams: () => { },
|
mapParams: () => {},
|
||||||
search: (params) => {
|
search: params => {
|
||||||
const studyUIDs = DicomMetadataStore.getStudyInstanceUIDs()
|
const studyUIDs = DicomMetadataStore.getStudyInstanceUIDs();
|
||||||
|
|
||||||
return studyUIDs.map(StudyInstanceUID => {
|
return studyUIDs.map(StudyInstanceUID => {
|
||||||
let numInstances = 0
|
let numInstances = 0;
|
||||||
const modalities = new Set()
|
const modalities = new Set();
|
||||||
|
|
||||||
// Calculating the number of instances in the study and modalities
|
// Calculating the number of instances in the study and modalities
|
||||||
// present in the study
|
// present in the study
|
||||||
const study = DicomMetadataStore.getStudy(StudyInstanceUID)
|
const study = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||||
study.series.forEach(aSeries => {
|
study.series.forEach(aSeries => {
|
||||||
numInstances += aSeries.instances.length
|
numInstances += aSeries.instances.length;
|
||||||
modalities.add(aSeries.Modality);
|
modalities.add(aSeries.Modality);
|
||||||
})
|
});
|
||||||
|
|
||||||
// first instance in the first series
|
// first instance in the first series
|
||||||
const firstInstance = study?.series[0]?.instances[0]
|
const firstInstance = study?.series[0]?.instances[0];
|
||||||
|
|
||||||
if (firstInstance) {
|
if (firstInstance) {
|
||||||
return {
|
return {
|
||||||
@ -80,33 +78,75 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
NumInstances: numInstances,
|
NumInstances: numInstances,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
processResults: () => {
|
processResults: () => {
|
||||||
console.debug(' DICOMLocal QUERY processResults')
|
console.debug(' DICOMLocal QUERY processResults');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
series: {
|
series: {
|
||||||
// mapParams: mapParams.bind(),
|
// mapParams: mapParams.bind(),
|
||||||
search: () => {
|
search: () => {
|
||||||
console.debug(' DICOMLocal QUERY SERIES SEARCH')
|
console.debug(' DICOMLocal QUERY SERIES SEARCH');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
instances: {
|
instances: {
|
||||||
search: () => {
|
search: () => {
|
||||||
console.debug(' DICOMLocal QUERY instances SEARCH')
|
console.debug(' DICOMLocal QUERY instances SEARCH');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
retrieve: {
|
retrieve: {
|
||||||
series: {
|
series: {
|
||||||
metaData: () => {
|
metaData: async ({ StudyInstanceUID, madeInClient = false } = {}) => {
|
||||||
console.debug(' DICOMLocal retrieve series metadata')
|
if (!StudyInstanceUID) {
|
||||||
|
throw new Error(
|
||||||
|
'Unable to query for SeriesMetadata without StudyInstanceUID'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instances metadata already added via local upload
|
||||||
|
const study = DicomMetadataStore.getStudy(
|
||||||
|
StudyInstanceUID,
|
||||||
|
madeInClient
|
||||||
|
);
|
||||||
|
|
||||||
|
// Series metadata already added via local upload
|
||||||
|
DicomMetadataStore._broadcastEvent(EVENTS.SERIES_ADDED, {
|
||||||
|
StudyInstanceUID,
|
||||||
|
madeInClient,
|
||||||
|
});
|
||||||
|
|
||||||
|
study.series.forEach(aSeries => {
|
||||||
|
const { SeriesInstanceUID } = aSeries;
|
||||||
|
|
||||||
|
aSeries.instances.forEach(instance => {
|
||||||
|
const {
|
||||||
|
url: imageId,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
} = instance;
|
||||||
|
|
||||||
|
// Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI.
|
||||||
|
metadataProvider.addImageIdToUIDs(imageId, {
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
DicomMetadataStore._broadcastEvent(EVENTS.INSTANCES_ADDED, {
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
madeInClient,
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
store: {
|
store: {
|
||||||
dicom: (naturalizedReport) => {
|
dicom: naturalizedReport => {
|
||||||
const reportBlob = dcmjs.data.datasetToBlob(naturalizedReport);
|
const reportBlob = dcmjs.data.datasetToBlob(naturalizedReport);
|
||||||
|
|
||||||
//Create a URL for the binary.
|
//Create a URL for the binary.
|
||||||
@ -114,91 +154,49 @@ function createDicomLocalApi(dicomLocalConfig) {
|
|||||||
window.location.assign(objectUrl);
|
window.location.assign(objectUrl);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
retrieveSeriesMetadata: async ({
|
|
||||||
StudyInstanceUID,
|
|
||||||
madeInClient = false,
|
|
||||||
} = {}) => {
|
|
||||||
if (!StudyInstanceUID) {
|
|
||||||
throw new Error(
|
|
||||||
'Unable to query for SeriesMetadata without StudyInstanceUID'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Instances metadata already added via local upload
|
|
||||||
const study = DicomMetadataStore.getStudy(StudyInstanceUID, madeInClient)
|
|
||||||
|
|
||||||
// Series metadata already added via local upload
|
|
||||||
DicomMetadataStore._broadcastEvent(EVENTS.SERIES_ADDED, {
|
|
||||||
StudyInstanceUID,
|
|
||||||
madeInClient,
|
|
||||||
})
|
|
||||||
|
|
||||||
study.series.forEach((aSeries) => {
|
|
||||||
const { SeriesInstanceUID } = aSeries
|
|
||||||
|
|
||||||
aSeries.instances.forEach((instance) => {
|
|
||||||
const {
|
|
||||||
url: imageId,
|
|
||||||
StudyInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
} = instance
|
|
||||||
|
|
||||||
// Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI.
|
|
||||||
metadataProvider.addImageIdToUIDs(imageId, {
|
|
||||||
StudyInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
SOPInstanceUID,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
DicomMetadataStore._broadcastEvent(EVENTS.INSTANCES_ADDED, {
|
|
||||||
StudyInstanceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
madeInClient,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
},
|
|
||||||
getImageIdsForDisplaySet(displaySet) {
|
getImageIdsForDisplaySet(displaySet) {
|
||||||
const images = displaySet.images
|
const images = displaySet.images;
|
||||||
const imageIds = []
|
const imageIds = [];
|
||||||
|
|
||||||
if (!images) {
|
if (!images) {
|
||||||
return imageIds
|
return imageIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
displaySet.images.forEach((instance) => {
|
displaySet.images.forEach(instance => {
|
||||||
const NumberOfFrames = instance.NumberOfFrames
|
const NumberOfFrames = instance.NumberOfFrames;
|
||||||
|
|
||||||
if (NumberOfFrames > 1) {
|
if (NumberOfFrames > 1) {
|
||||||
for (let i = 0; i < NumberOfFrames; i++) {
|
for (let i = 0; i < NumberOfFrames; i++) {
|
||||||
const imageId = this.getImageIdsForInstance({
|
const imageId = this.getImageIdsForInstance({
|
||||||
instance,
|
instance,
|
||||||
frame: i,
|
frame: i,
|
||||||
})
|
});
|
||||||
imageIds.push(imageId)
|
imageIds.push(imageId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const imageId = this.getImageIdsForInstance({ instance })
|
const imageId = this.getImageIdsForInstance({ instance });
|
||||||
imageIds.push(imageId)
|
imageIds.push(imageId);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return imageIds
|
return imageIds;
|
||||||
},
|
},
|
||||||
getImageIdsForInstance({ instance, frame }) {
|
getImageIdsForInstance({ instance, frame }) {
|
||||||
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance
|
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
|
||||||
const storedInstance = DicomMetadataStore.getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID)
|
const storedInstance = DicomMetadataStore.getInstance(
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID
|
||||||
|
);
|
||||||
if (storedInstance.url) {
|
if (storedInstance.url) {
|
||||||
return storedInstance.url
|
return storedInstance.url;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteStudyMetadataPromise() {
|
deleteStudyMetadataPromise() {
|
||||||
console.log("deleteStudyMetadataPromise not implemented")
|
console.log('deleteStudyMetadataPromise not implemented');
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
return IWebApiDataSource.create(implementation)
|
return IWebApiDataSource.create(implementation);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createDicomLocalApi }
|
export { createDicomLocalApi };
|
||||||
|
|||||||
@ -7,7 +7,12 @@ import {
|
|||||||
processSeriesResults,
|
processSeriesResults,
|
||||||
} from './qido.js';
|
} from './qido.js';
|
||||||
import dcm4cheeReject from './dcm4cheeReject';
|
import dcm4cheeReject from './dcm4cheeReject';
|
||||||
import { DicomMetadataStore, IWebApiDataSource, utils, errorHandler } from '@ohif/core';
|
import {
|
||||||
|
DicomMetadataStore,
|
||||||
|
IWebApiDataSource,
|
||||||
|
utils,
|
||||||
|
errorHandler,
|
||||||
|
} from '@ohif/core';
|
||||||
|
|
||||||
import getImageId from './utils/getImageId';
|
import getImageId from './utils/getImageId';
|
||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
@ -65,7 +70,9 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
|
|
||||||
// TODO -> Two clients sucks, but its better than 1000.
|
// TODO -> Two clients sucks, but its better than 1000.
|
||||||
// TODO -> We'll need to merge auth later.
|
// TODO -> We'll need to merge auth later.
|
||||||
const qidoDicomWebClient = staticWado ? new StaticWadoClient(qidoConfig) : new api.DICOMwebClient(qidoConfig);
|
const qidoDicomWebClient = staticWado
|
||||||
|
? new StaticWadoClient(qidoConfig)
|
||||||
|
: new api.DICOMwebClient(qidoConfig);
|
||||||
const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig);
|
const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig);
|
||||||
|
|
||||||
const implementation = {
|
const implementation = {
|
||||||
@ -84,7 +91,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
query: {
|
query: {
|
||||||
studies: {
|
studies: {
|
||||||
mapParams: mapParams.bind(),
|
mapParams: mapParams.bind(),
|
||||||
search: async function (origParams) {
|
search: async function(origParams) {
|
||||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
if (headers) {
|
if (headers) {
|
||||||
qidoDicomWebClient.headers = headers;
|
qidoDicomWebClient.headers = headers;
|
||||||
@ -109,7 +116,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
},
|
},
|
||||||
series: {
|
series: {
|
||||||
// mapParams: mapParams.bind(),
|
// mapParams: mapParams.bind(),
|
||||||
search: async function (studyInstanceUid) {
|
search: async function(studyInstanceUid) {
|
||||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
if (headers) {
|
if (headers) {
|
||||||
qidoDicomWebClient.headers = headers;
|
qidoDicomWebClient.headers = headers;
|
||||||
@ -143,85 +150,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
},
|
},
|
||||||
retrieve: {
|
retrieve: {
|
||||||
series: {
|
series: {
|
||||||
// TODO: change queryParams to `StudyInstanceUID` for now?
|
metadata: async ({
|
||||||
// Conduct query, return a promise like others
|
|
||||||
// Await this call and add to DicomMetadataStore after receiving result
|
|
||||||
metadata: (queryParams, callback) => {
|
|
||||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
|
||||||
if (headers) {
|
|
||||||
wadoDicomWebClient.headers = headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { StudyInstanceUIDs } = urlUtil.parse(queryParams, true);
|
|
||||||
|
|
||||||
StudyInstanceUIDs = urlUtil.paramString.parseParam(StudyInstanceUIDs);
|
|
||||||
|
|
||||||
if (!StudyInstanceUIDs) {
|
|
||||||
throw new Error(
|
|
||||||
'Incomplete queryParams, missing StudyInstanceUIDs'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const storeInstances = instances => {
|
|
||||||
const naturalizedInstances = instances.map(naturalizeDataset);
|
|
||||||
DicomMetadataStore.addInstances(naturalizedInstances);
|
|
||||||
callback(naturalizedInstances);
|
|
||||||
};
|
|
||||||
const studyPromises = StudyInstanceUIDs.map(StudyInstanceUID =>
|
|
||||||
retrieveStudyMetadata(
|
|
||||||
wadoDicomWebClient,
|
|
||||||
StudyInstanceUID,
|
|
||||||
enableStudyLazyLoad
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
studyPromises.forEach(studyPromise => {
|
|
||||||
studyPromise.then(data => {
|
|
||||||
const { seriesPromises } = data;
|
|
||||||
seriesPromises.forEach(seriesPromise => {
|
|
||||||
seriesPromise.then(instances => {
|
|
||||||
storeInstances(instances);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
store: {
|
|
||||||
dicom: async dataset => {
|
|
||||||
const headers = UserAuthenticationService.getAuthorizationHeader();
|
|
||||||
if (headers) {
|
|
||||||
wadoDicomWebClient.headers = headers;
|
|
||||||
}
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
FileMetaInformationVersion:
|
|
||||||
dataset._meta.FileMetaInformationVersion.Value,
|
|
||||||
MediaStorageSOPClassUID: dataset.SOPClassUID,
|
|
||||||
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
|
|
||||||
TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN,
|
|
||||||
ImplementationClassUID,
|
|
||||||
ImplementationVersionName,
|
|
||||||
};
|
|
||||||
|
|
||||||
const denaturalized = denaturalizeDataset(meta);
|
|
||||||
const dicomDict = new DicomDict(denaturalized);
|
|
||||||
|
|
||||||
dicomDict.dict = denaturalizeDataset(dataset);
|
|
||||||
|
|
||||||
const part10Buffer = dicomDict.write();
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
datasets: [part10Buffer],
|
|
||||||
};
|
|
||||||
|
|
||||||
await wadoDicomWebClient.storeInstances(options);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// TODO: Rename this it makes no sense at all
|
|
||||||
retrieveSeriesMetadata: async ({
|
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
filters,
|
filters,
|
||||||
sortCriteria,
|
sortCriteria,
|
||||||
@ -279,15 +208,17 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
// any implementation that stores static copies of the metadata
|
// any implementation that stores static copies of the metadata
|
||||||
StudyInstanceUID: naturalized.StudyInstanceUID,
|
StudyInstanceUID: naturalized.StudyInstanceUID,
|
||||||
};
|
};
|
||||||
return qidoDicomWebClient.retrieveBulkData(options).then(val => {
|
return qidoDicomWebClient
|
||||||
const ret = val && val[0] || undefined;
|
.retrieveBulkData(options)
|
||||||
|
.then(val => {
|
||||||
|
const ret = (val && val[0]) || undefined;
|
||||||
value.Value = ret;
|
value.Value = ret;
|
||||||
return ret;
|
return ret;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return naturalized
|
return naturalized;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Async load series, store as retrieved
|
// Async load series, store as retrieved
|
||||||
@ -307,9 +238,14 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
|
|
||||||
// Google Cloud Healthcare doesn't return StudyInstanceUID, so we need to add
|
// Google Cloud Healthcare doesn't return StudyInstanceUID, so we need to add
|
||||||
// it manually here
|
// it manually here
|
||||||
seriesSummaryMetadata.forEach(aSeries => { aSeries.StudyInstanceUID = StudyInstanceUID })
|
seriesSummaryMetadata.forEach(aSeries => {
|
||||||
|
aSeries.StudyInstanceUID = StudyInstanceUID;
|
||||||
|
});
|
||||||
|
|
||||||
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
|
DicomMetadataStore.addSeriesMetadata(
|
||||||
|
seriesSummaryMetadata,
|
||||||
|
madeInClient
|
||||||
|
);
|
||||||
|
|
||||||
const numberOfSeries = seriesPromises.length;
|
const numberOfSeries = seriesPromises.length;
|
||||||
seriesPromises.forEach(async (seriesPromise, index) => {
|
seriesPromises.forEach(async (seriesPromise, index) => {
|
||||||
@ -318,6 +254,39 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
|
|||||||
if (index === numberOfSeries - 1) setSuccessFlag();
|
if (index === numberOfSeries - 1) setSuccessFlag();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
store: {
|
||||||
|
dicom: async dataset => {
|
||||||
|
const headers = UserAuthenticationService.getAuthorizationHeader();
|
||||||
|
if (headers) {
|
||||||
|
wadoDicomWebClient.headers = headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
FileMetaInformationVersion:
|
||||||
|
dataset._meta.FileMetaInformationVersion.Value,
|
||||||
|
MediaStorageSOPClassUID: dataset.SOPClassUID,
|
||||||
|
MediaStorageSOPInstanceUID: dataset.SOPInstanceUID,
|
||||||
|
TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN,
|
||||||
|
ImplementationClassUID,
|
||||||
|
ImplementationVersionName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const denaturalized = denaturalizeDataset(meta);
|
||||||
|
const dicomDict = new DicomDict(denaturalized);
|
||||||
|
|
||||||
|
dicomDict.dict = denaturalizeDataset(dataset);
|
||||||
|
|
||||||
|
const part10Buffer = dicomDict.write();
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
datasets: [part10Buffer],
|
||||||
|
};
|
||||||
|
|
||||||
|
await wadoDicomWebClient.storeInstances(options);
|
||||||
|
},
|
||||||
|
},
|
||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
getImageIdsForDisplaySet(displaySet) {
|
getImageIdsForDisplaySet(displaySet) {
|
||||||
const images = displaySet.images;
|
const images = displaySet.images;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ function requestDisplaySetCreationForStudy(
|
|||||||
dataSource,
|
dataSource,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
madeInClient,
|
madeInClient
|
||||||
) {
|
) {
|
||||||
// TODO: is this already short-circuited by the map of Retrieve promises?
|
// TODO: is this already short-circuited by the map of Retrieve promises?
|
||||||
if (
|
if (
|
||||||
@ -13,7 +13,7 @@ function requestDisplaySetCreationForStudy(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSource.retrieveSeriesMetadata({ StudyInstanceUID, madeInClient });
|
dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default requestDisplaySetCreationForStudy;
|
export default requestDisplaySetCreationForStudy;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ function requestDisplaySetCreationForStudy(
|
|||||||
dataSource,
|
dataSource,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
madeInClient,
|
madeInClient
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
DisplaySetService.activeDisplaySets.some(
|
DisplaySetService.activeDisplaySets.some(
|
||||||
@ -12,7 +12,7 @@ function requestDisplaySetCreationForStudy(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dataSource.retrieveSeriesMetadata({ StudyInstanceUID, madeInClient });
|
dataSource.retrieve.series.metadata({ StudyInstanceUID, madeInClient });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default requestDisplaySetCreationForStudy;
|
export default requestDisplaySetCreationForStudy;
|
||||||
|
|||||||
@ -18,7 +18,6 @@ function create({
|
|||||||
store,
|
store,
|
||||||
reject,
|
reject,
|
||||||
initialize,
|
initialize,
|
||||||
retrieveSeriesMetadata,
|
|
||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
getImageIdsForDisplaySet,
|
getImageIdsForDisplaySet,
|
||||||
getImageIdsForInstance,
|
getImageIdsForInstance,
|
||||||
@ -38,7 +37,7 @@ function create({
|
|||||||
* @param {number} params.resultsPerPage
|
* @param {number} params.resultsPerPage
|
||||||
*/
|
*/
|
||||||
mapParams: params => params,
|
mapParams: params => params,
|
||||||
requestResults: () => { },
|
requestResults: () => {},
|
||||||
processResults: results => results,
|
processResults: results => results,
|
||||||
},
|
},
|
||||||
series: {},
|
series: {},
|
||||||
@ -64,7 +63,6 @@ function create({
|
|||||||
reject: reject || defaultReject,
|
reject: reject || defaultReject,
|
||||||
store: store || defaultStore,
|
store: store || defaultStore,
|
||||||
initialize,
|
initialize,
|
||||||
retrieveSeriesMetadata,
|
|
||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
getImageIdsForDisplaySet,
|
getImageIdsForDisplaySet,
|
||||||
getImageIdsForInstance,
|
getImageIdsForInstance,
|
||||||
|
|||||||
@ -2,17 +2,27 @@
|
|||||||
sidebar_position: 3
|
sidebar_position: 3
|
||||||
sidebar_label: Data Source
|
sidebar_label: Data Source
|
||||||
---
|
---
|
||||||
|
|
||||||
# Module: Data Source
|
# Module: Data Source
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
## 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 format, which we call DataSources, and have provided one implementation of this standard.
|
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.
|
||||||
|
|
||||||
You can make another datasource implementation which communicates to your backend and maps to OHIF’s native format, then use any existing mode on your platform. Your data doesn’t even need to be DICOM if you can map some proprietary data to the correct format.
|
We have built a standard for fetching and mapping data into OHIF’s native
|
||||||
|
format, which we call DataSources, and have provided one implementation of this
|
||||||
|
standard.
|
||||||
|
|
||||||
The DataSource is also a place to add easy helper methods that platform-specific extensions can call in order to interact with the backend, meaning proprietary data interactions can be wrapped in extensions.
|
You can make another datasource implementation which communicates to your
|
||||||
|
backend and maps to OHIF’s native format, then use any existing mode on your
|
||||||
|
platform. Your data doesn’t even need to be DICOM if you can map some
|
||||||
|
proprietary data to the correct format.
|
||||||
|
|
||||||
|
The DataSource is also a place to add easy helper methods that platform-specific
|
||||||
|
extensions can call in order to interact with the backend, meaning proprietary
|
||||||
|
data interactions can be wrapped in extensions.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const getDataSourcesModule = () => [
|
const getDataSourcesModule = () => [
|
||||||
@ -26,13 +36,13 @@ const getDataSourcesModule = () => [
|
|||||||
];
|
];
|
||||||
```
|
```
|
||||||
|
|
||||||
Default extension provides two main data sources that are commonly used: `dicomweb` and `dicomjson`
|
Default extension provides two main data sources that are commonly used:
|
||||||
|
`dicomweb` and `dicomjson`
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import { createDicomWebApi } from './DicomWebDataSource/index.js';
|
import { createDicomWebApi } from './DicomWebDataSource/index.js';
|
||||||
import { createDicomJSONApi } from './DicomJSONDataSource/index.js';
|
import { createDicomJSONApi } from './DicomJSONDataSource/index.js';
|
||||||
|
|
||||||
|
|
||||||
function getDataSourcesModule() {
|
function getDataSourcesModule() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@ -49,13 +59,14 @@ function getDataSourcesModule() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Custom DataSource
|
## Custom DataSource
|
||||||
You can add your custom datasource by creating the implementation using `IWebApiDataSource.create` from `@ohif/core`. This factory function creates a new "Web API" data source that fetches data over HTTP.
|
|
||||||
|
|
||||||
You need to make sure, you implement the following functions for the data source.
|
|
||||||
|
|
||||||
|
You can add your custom datasource by creating the implementation using
|
||||||
|
`IWebApiDataSource.create` from `@ohif/core`. This factory function creates a
|
||||||
|
new "Web API" data source that fetches data over HTTP.
|
||||||
|
|
||||||
|
You need to make sure, you implement the following functions for the data
|
||||||
|
source.
|
||||||
|
|
||||||
```js title="platform/core/src/DataSources/IWebApiDataSource.js"
|
```js title="platform/core/src/DataSources/IWebApiDataSource.js"
|
||||||
function create({
|
function create({
|
||||||
@ -64,7 +75,6 @@ function create({
|
|||||||
store,
|
store,
|
||||||
reject,
|
reject,
|
||||||
parseRouteParams,
|
parseRouteParams,
|
||||||
retrieveSeriesMetadata,
|
|
||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
getImageIdsForDisplaySet,
|
getImageIdsForDisplaySet,
|
||||||
getImageIdsForInstance,
|
getImageIdsForInstance,
|
||||||
@ -79,8 +89,8 @@ You can take a look at `dicomweb` data source implementation to get an idea
|
|||||||
## Static WADO Client
|
## Static WADO Client
|
||||||
|
|
||||||
If the configuration for the data source has the value staticWado set, then it
|
If the configuration for the data source has the value staticWado set, then it
|
||||||
is assumed that queries for the studies return a super-set of the studies, as
|
is assumed that queries for the studies return a super-set of the studies, as it
|
||||||
it is assumed to be returning a static list. The StaticWadoClient performs the
|
is assumed to be returning a static list. The StaticWadoClient performs the
|
||||||
search functionality manually, by interpretting the query parameters and then
|
search functionality manually, by interpretting the query parameters and then
|
||||||
applying them to the returned response. This functionality may be useful for
|
applying them to the returned response. This functionality may be useful for
|
||||||
other types of DICOMweb back ends, where they are capable of performing queries,
|
other types of DICOMweb back ends, where they are capable of performing queries,
|
||||||
@ -89,5 +99,8 @@ as long as the size of the studies list isn't too large that client side
|
|||||||
selectiton isn't too expensive.
|
selectiton isn't too expensive.
|
||||||
|
|
||||||
## DicomMetadataStore
|
## DicomMetadataStore
|
||||||
In `OHIF-v3` we have a central location for the metadata of studies and they are located
|
|
||||||
in `DicomMetadataStore`. Your custom datasource can communicate with `DicomMetadataStore` to store, and fetch Study/Series/Instance metadata. We will learn more about `DicomMetadataStore` in services.
|
In `OHIF-v3` we have a central location for the metadata of studies and they are
|
||||||
|
located in `DicomMetadataStore`. Your custom datasource can communicate with
|
||||||
|
`DicomMetadataStore` to store, and fetch Study/Series/Instance metadata. We will
|
||||||
|
learn more about `DicomMetadataStore` in services.
|
||||||
|
|||||||
@ -2,22 +2,30 @@
|
|||||||
sidebar_position: 3
|
sidebar_position: 3
|
||||||
sidebar_label: Routes
|
sidebar_label: Routes
|
||||||
---
|
---
|
||||||
|
|
||||||
# Mode: Routes
|
# Mode: Routes
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Modes are tied to a specific route in the viewer, and multiple modes/routes can be present within a single application. This makes `routes` config, THE most important part of the mode configuration.
|
Modes are tied to a specific route in the viewer, and multiple modes/routes can
|
||||||
|
be present within a single application. This makes `routes` config, THE most
|
||||||
|
important part of the mode configuration.
|
||||||
|
|
||||||
## Route
|
## Route
|
||||||
`@ohif/viewer` **compose** extensions to build applications on different routes for the platform.
|
|
||||||
|
|
||||||
Below, you can see a simplified version of the `longitudinal` mode and the `routes` section
|
`@ohif/viewer` **compose** extensions to build applications on different routes
|
||||||
which has defined one `route`. Each route has three different configuration:
|
for the platform.
|
||||||
|
|
||||||
- **route path**: defines the route path to access the built application for that route
|
Below, you can see a simplified version of the `longitudinal` mode and the
|
||||||
- **route init**: hook that runs when application enters the defined route path, if not defined the default init function will run for the mode.
|
`routes` section which has defined one `route`. Each route has three different
|
||||||
- **route layout**: defines the layout of the application for the specified route (panels, viewports)
|
configuration:
|
||||||
|
|
||||||
|
- **route path**: defines the route path to access the built application for
|
||||||
|
that route
|
||||||
|
- **route init**: hook that runs when application enters the defined route path,
|
||||||
|
if not defined the default init function will run for the mode.
|
||||||
|
- **route layout**: defines the layout of the application for the specified
|
||||||
|
route (panels, viewports)
|
||||||
|
|
||||||
```js
|
```js
|
||||||
export default function mode() {
|
export default function mode() {
|
||||||
@ -56,51 +64,61 @@ export default function mode() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
/*
|
/*
|
||||||
...
|
...
|
||||||
*/
|
*/
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Route: path
|
### Route: path
|
||||||
Upon initialization the viewer will consume extensions and modes and build up the route desired, these can then be accessed via the study list, or directly via url parameters.
|
|
||||||
|
|
||||||
> Note: Currently, only one route is built for each mode, but we will enhance route
|
Upon initialization the viewer will consume extensions and modes and build up
|
||||||
> creation to create separate routes based on the `path` config for each `route` object.
|
the route desired, these can then be accessed via the study list, or directly
|
||||||
|
via url parameters.
|
||||||
|
|
||||||
|
> Note: Currently, only one route is built for each mode, but we will enhance
|
||||||
|
> route creation to create separate routes based on the `path` config for each
|
||||||
|
> `route` object.
|
||||||
|
|
||||||
There are two types of `routes` that are created by the mode.
|
There are two types of `routes` that are created by the mode.
|
||||||
|
|
||||||
- Routes with dataSourceName `/${mode.id}/${dataSourceName}`
|
- Routes with dataSourceName `/${mode.id}/${dataSourceName}`
|
||||||
- Routes without dataSourceName `/${mode.id}`
|
- Routes without dataSourceName `/${mode.id}`
|
||||||
|
|
||||||
Therefore navigating to `http://localhost:3000/viewer/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1` will run the app with the layout and functionalities of the `viewer` mode using the `defaultDataSourceName` which is defined in the [App Config](../../configuration/index.md)
|
Therefore navigating to
|
||||||
|
`http://localhost:3000/viewer/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1`
|
||||||
You can use the same exact mode using a different registered data source (e.g., `dicomjson`) by navigating to `http://localhost:3000/viewer/dicomjson/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1`
|
will run the app with the layout and functionalities of the `viewer` mode using
|
||||||
|
the `defaultDataSourceName` which is defined in the
|
||||||
|
[App Config](../../configuration/index.md)
|
||||||
|
|
||||||
|
You can use the same exact mode using a different registered data source (e.g.,
|
||||||
|
`dicomjson`) by navigating to
|
||||||
|
`http://localhost:3000/viewer/dicomjson/?StudyInstanceUIDs=1.3.6.1.4.1.25403.345050719074.3824.20170125113417.1`
|
||||||
|
|
||||||
### Route: init
|
### Route: init
|
||||||
The mode also has an init hook, which initializes the mode. If you don't define an `init`
|
|
||||||
function the `default init` function will get run (logic is located inside `Mode.jsx`). However, you
|
The mode also has an init hook, which initializes the mode. If you don't define
|
||||||
can define you own init function following certain steps which we will discuss next.
|
an `init` function the `default init` function will get run (logic is located
|
||||||
|
inside `Mode.jsx`). However, you can define you own init function following
|
||||||
|
certain steps which we will discuss next.
|
||||||
|
|
||||||
#### Default init
|
#### Default init
|
||||||
|
|
||||||
Default init function will:
|
Default init function will:
|
||||||
|
|
||||||
- `retriveSeriesMetaData` for the `studyInstanceUIDs` that are defined in the URL.
|
- `retriveSeriesMetaData` for the `studyInstanceUIDs` that are defined in the
|
||||||
- Subscribe to `instanceAdded` event, to make display sets after a series have finished
|
URL.
|
||||||
retrieving its instances metadata.
|
- Subscribe to `instanceAdded` event, to make display sets after a series have
|
||||||
- Subscribe to `seriesAdded` event, to run the `HangingProtocolService` on the retrieves series
|
finished retrieving its instances metadata.
|
||||||
from the study.
|
- Subscribe to `seriesAdded` event, to run the `HangingProtocolService` on the
|
||||||
|
retrieves series from the study.
|
||||||
|
|
||||||
|
A _simplified_ "pseudocode" for the `defaultRouteInit` is:
|
||||||
A *simplified* "pseudocode" for the `defaultRouteInit` is:
|
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
async function defaultRouteInit({
|
async function defaultRouteInit({
|
||||||
@ -108,7 +126,10 @@ async function defaultRouteInit({
|
|||||||
studyInstanceUIDs,
|
studyInstanceUIDs,
|
||||||
dataSource,
|
dataSource,
|
||||||
}) {
|
}) {
|
||||||
const { DisplaySetService, HangingProtocolService } = servicesManager.services
|
const {
|
||||||
|
DisplaySetService,
|
||||||
|
HangingProtocolService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
// subscribe to run the function after the event happens
|
// subscribe to run the function after the event happens
|
||||||
DicomMetadataStore.subscribe(
|
DicomMetadataStore.subscribe(
|
||||||
@ -117,26 +138,26 @@ async function defaultRouteInit({
|
|||||||
const seriesMetadata = DicomMetadataStore.getSeries(
|
const seriesMetadata = DicomMetadataStore.getSeries(
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID
|
SeriesInstanceUID
|
||||||
)
|
);
|
||||||
DisplaySetService.makeDisplaySets(seriesMetadata.instances)
|
DisplaySetService.makeDisplaySets(seriesMetadata.instances);
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
studyInstanceUIDs.forEach((StudyInstanceUID) => {
|
studyInstanceUIDs.forEach(StudyInstanceUID => {
|
||||||
dataSource.retrieveSeriesMetadata({ StudyInstanceUID })
|
dataSource.retrieve.series.metadata({ StudyInstanceUID });
|
||||||
})
|
});
|
||||||
|
|
||||||
DicomMetadataStore.subscribe('seriesAdded', ({ StudyInstanceUID }) => {
|
DicomMetadataStore.subscribe('seriesAdded', ({ StudyInstanceUID }) => {
|
||||||
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID)
|
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||||
HangingProtocolService.run(studyMetadata)
|
HangingProtocolService.run(studyMetadata);
|
||||||
})
|
});
|
||||||
|
|
||||||
return unsubscriptions
|
return unsubscriptions;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Writing a custom init
|
#### Writing a custom init
|
||||||
|
|
||||||
You can add your custom init function to enhance the default initialization for:
|
You can add your custom init function to enhance the default initialization for:
|
||||||
|
|
||||||
- Fetching annotations from a server for the current study
|
- Fetching annotations from a server for the current study
|
||||||
@ -146,11 +167,13 @@ You can add your custom init function to enhance the default initialization for:
|
|||||||
|
|
||||||
and lots of other modifications.
|
and lots of other modifications.
|
||||||
|
|
||||||
You just need to make sure, the mode `retrieveSeriesMetadata`, `makeDisplaySets` and `run` the
|
You just need to make sure, the mode `dataSource.retrieve.series.metadata`,
|
||||||
HangingProtocols at some point. There are various `events` that you can subscribe to and add your custom logic. **point to events**
|
`makeDisplaySets` and `run` the HangingProtocols at some point. There are
|
||||||
|
various `events` that you can subscribe to and add your custom logic. **point to
|
||||||
For instance for jumping to the slice where a measurement is located at the initial render, you need to follow a pattern similar to the following:
|
events**
|
||||||
|
|
||||||
|
For instance for jumping to the slice where a measurement is located at the
|
||||||
|
initial render, you need to follow a pattern similar to the following:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
init: async ({
|
init: async ({
|
||||||
@ -160,64 +183,68 @@ init: async ({
|
|||||||
dataSource,
|
dataSource,
|
||||||
studyInstanceUIDs,
|
studyInstanceUIDs,
|
||||||
}) => {
|
}) => {
|
||||||
const { DisplaySetService } = servicesManager.services
|
const { DisplaySetService } = servicesManager.services;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
...
|
...
|
||||||
**/
|
**/
|
||||||
|
|
||||||
const onDisplaySetsAdded = ({ displaySetsAdded, options }) => {
|
const onDisplaySetsAdded = ({ displaySetsAdded, options }) => {
|
||||||
const displaySet = displaySetsAdded[0]
|
const displaySet = displaySetsAdded[0];
|
||||||
const { SeriesInstanceUID } = displaySet
|
const { SeriesInstanceUID } = displaySet;
|
||||||
|
|
||||||
const toolData = myServer.fetchMeasurements(SeriesInstanceUID)
|
const toolData = myServer.fetchMeasurements(SeriesInstanceUID);
|
||||||
|
|
||||||
if (!toolData.length) {
|
if (!toolData.length) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toolData.forEach((tool) => {
|
toolData.forEach(tool => {
|
||||||
const instance = displaySet.images.find(
|
const instance = displaySet.images.find(
|
||||||
(image) => image.SOPInstanceUID === tool.SOPInstanceUID
|
image => image.SOPInstanceUID === tool.SOPInstanceUID
|
||||||
)
|
);
|
||||||
|
|
||||||
const { SOPInstanceUID, url } = instance
|
const { SOPInstanceUID, url } = instance;
|
||||||
displaySet.initialImageIdIndex = displaySet.images.indexOf(instance)
|
displaySet.initialImageIdIndex = displaySet.images.indexOf(instance);
|
||||||
})
|
});
|
||||||
|
|
||||||
MeasurementService.addMeasurement(/**...**/)
|
MeasurementService.addMeasurement(/**...**/);
|
||||||
}
|
};
|
||||||
|
|
||||||
// subscription to the DISPLAY_SETS_ADDED
|
// subscription to the DISPLAY_SETS_ADDED
|
||||||
const { unsubscribe } = DisplaySetService.subscribe(
|
const { unsubscribe } = DisplaySetService.subscribe(
|
||||||
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||||
onDisplaySetsAdded
|
onDisplaySetsAdded
|
||||||
)
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
...
|
...
|
||||||
**/
|
**/
|
||||||
|
|
||||||
return unsubscriptions
|
return unsubscriptions;
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Route: layoutTemplate
|
### Route: layoutTemplate
|
||||||
`layoutTemplate` is the last configuration for a certain route in a `mode`. `layoutTemplate` is
|
|
||||||
a function that returns an object that configures the overall layout of the application. The returned
|
|
||||||
object has two properties:
|
|
||||||
|
|
||||||
- `id`: the id of the `layoutTemplate` being used (it should have been registered via an extension)
|
`layoutTemplate` is the last configuration for a certain route in a `mode`.
|
||||||
|
`layoutTemplate` is a function that returns an object that configures the
|
||||||
|
overall layout of the application. The returned object has two properties:
|
||||||
|
|
||||||
|
- `id`: the id of the `layoutTemplate` being used (it should have been
|
||||||
|
registered via an extension)
|
||||||
- `props`: the required properties to be passed to the `layoutTemplate`.
|
- `props`: the required properties to be passed to the `layoutTemplate`.
|
||||||
|
|
||||||
For instance `default extension` provides a layoutTemplate that builds the app using left/right panels
|
For instance `default extension` provides a layoutTemplate that builds the app
|
||||||
and viewports. Therefore, the `props` include `leftPanels`, `rightPanels` and `viewports` sections. Note that the `layoutTemplate` defines the properties it is expecting. So, if you write a `layoutTemplate-2` that accepts a footer section, its logic should be written in the extension, and any mode that
|
using left/right panels and viewports. Therefore, the `props` include
|
||||||
is interested in using `layoutTemplate-2` **should** provide the `id` for the footer component.
|
`leftPanels`, `rightPanels` and `viewports` sections. Note that the
|
||||||
|
`layoutTemplate` defines the properties it is expecting. So, if you write a
|
||||||
|
`layoutTemplate-2` that accepts a footer section, its logic should be written in
|
||||||
|
the extension, and any mode that is interested in using `layoutTemplate-2`
|
||||||
|
**should** provide the `id` for the footer component.
|
||||||
|
|
||||||
**What module should the footer be registered?**
|
**What module should the footer be registered?**
|
||||||
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
/*
|
/*
|
||||||
...
|
...
|
||||||
@ -242,28 +269,26 @@ layoutTemplate: ({ location, servicesManager }) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
/*
|
/*
|
||||||
...
|
...
|
||||||
*/
|
*/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
> What is the difference between `onModeEnter` and `route.init`
|
> What is the difference between `onModeEnter` and `route.init`
|
||||||
|
|
||||||
`onModeEnter` gets run first than `route.init`; however, each route can have their own `init`, but they share the `onModeEnter`.
|
`onModeEnter` gets run first than `route.init`; however, each route can have
|
||||||
|
their own `init`, but they share the `onModeEnter`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
> How can I change the `workList` appearance or add a new login page?
|
> How can I change the `workList` appearance or add a new login page?
|
||||||
|
|
||||||
This is where `OHIF-v3` shines! Since the default `layoutTemplate` is written for the viewer part, you can simply add a new `layoutTemplate` and use the component you have written for that route. `Mode` handle showing the correct component for the specified route.
|
This is where `OHIF-v3` shines! Since the default `layoutTemplate` is written
|
||||||
|
for the viewer part, you can simply add a new `layoutTemplate` and use the
|
||||||
|
component you have written for that route. `Mode` handle showing the correct
|
||||||
|
component for the specified route.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
export default function mode() {
|
export default function mode() {
|
||||||
@ -278,15 +303,15 @@ export default function mode() {
|
|||||||
return {
|
return {
|
||||||
id: 'worklistLayout',
|
id: 'worklistLayout',
|
||||||
props: {
|
props: {
|
||||||
component: 'myNewWorkList'
|
component: 'myNewWorkList',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
/*
|
/*
|
||||||
...
|
...
|
||||||
*/
|
*/
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -2,20 +2,25 @@
|
|||||||
sidebar_position: 4
|
sidebar_position: 4
|
||||||
sidebar_label: Pub Sub
|
sidebar_label: Pub Sub
|
||||||
---
|
---
|
||||||
|
|
||||||
# Pub sub
|
# Pub sub
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
Publish–subscribe pattern is a messaging pattern that is one of the fundamentals patterns used in reusable software components.
|
|
||||||
|
|
||||||
In short, services that implements this pattern, can have listeners subscribed to their broadcasted events. After the event is fired, the corresponding listener will execute the function that is registered.
|
Publish–subscribe pattern is a messaging pattern that is one of the fundamentals
|
||||||
|
patterns used in reusable software components.
|
||||||
|
|
||||||
You can read more about this design pattern [here](https://cloud.google.com/pubsub/docs/overview).
|
In short, services that implements this pattern, can have listeners subscribed
|
||||||
|
to their broadcasted events. After the event is fired, the corresponding
|
||||||
|
listener will execute the function that is registered.
|
||||||
|
|
||||||
|
You can read more about this design pattern
|
||||||
|
[here](https://cloud.google.com/pubsub/docs/overview).
|
||||||
|
|
||||||
## Example: Default Initialization
|
## Example: Default Initialization
|
||||||
In `Mode.jsx` we have a default initialization that demonstrates
|
|
||||||
a series of subscriptions to various events.
|
|
||||||
|
|
||||||
|
In `Mode.jsx` we have a default initialization that demonstrates a series of
|
||||||
|
subscriptions to various events.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
async function defaultRouteInit({
|
async function defaultRouteInit({
|
||||||
@ -47,7 +52,7 @@ async function defaultRouteInit({
|
|||||||
unsubscriptions.push(instanceAddedUnsubscribe);
|
unsubscriptions.push(instanceAddedUnsubscribe);
|
||||||
|
|
||||||
studyInstanceUIDs.forEach(StudyInstanceUID => {
|
studyInstanceUIDs.forEach(StudyInstanceUID => {
|
||||||
dataSource.retrieveSeriesMetadata({ StudyInstanceUID });
|
dataSource.retrieve.series.metadata({ StudyInstanceUID });
|
||||||
});
|
});
|
||||||
|
|
||||||
const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe(
|
const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe(
|
||||||
@ -64,50 +69,52 @@ async function defaultRouteInit({
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Unsubscription
|
## Unsubscription
|
||||||
You need to be careful if you are adding custom subscriptions to the app. Each subscription will return a unsubscription function that needs to be executed on component destruction to avoid adding multiple subscriptions to the same observer.
|
|
||||||
|
|
||||||
Below, we can see `simplified` `Mode.jsx` and the corresponding `useEffect` where the unsubscription functions are executed upon destruction.
|
You need to be careful if you are adding custom subscriptions to the app. Each
|
||||||
|
subscription will return a unsubscription function that needs to be executed on
|
||||||
|
component destruction to avoid adding multiple subscriptions to the same
|
||||||
|
observer.
|
||||||
|
|
||||||
|
Below, we can see `simplified` `Mode.jsx` and the corresponding `useEffect`
|
||||||
|
where the unsubscription functions are executed upon destruction.
|
||||||
|
|
||||||
```js title="platform/viewer/src/routes/Mode/Mode.jsx"
|
```js title="platform/viewer/src/routes/Mode/Mode.jsx"
|
||||||
export default function ModeRoute(/**..**/) {
|
export default function ModeRoute(/**..**/) {
|
||||||
/**...**/
|
/**...**/
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => {
|
|
||||||
/**...**/
|
/**...**/
|
||||||
|
|
||||||
DisplaySetService.init(extensionManager, sopClassHandlers)
|
DisplaySetService.init(extensionManager, sopClassHandlers);
|
||||||
|
|
||||||
extensionManager.onModeEnter()
|
extensionManager.onModeEnter();
|
||||||
mode?.onModeEnter({ servicesManager, extensionManager })
|
mode?.onModeEnter({ servicesManager, extensionManager });
|
||||||
|
|
||||||
hangingProtocols.forEach((extentionProtocols) => {
|
hangingProtocols.forEach(extentionProtocols => {
|
||||||
const { protocols } =
|
const { protocols } = extensionManager.getModuleEntry(extentionProtocols);
|
||||||
extensionManager.getModuleEntry(extentionProtocols)
|
HangingProtocolService.addProtocols(protocols);
|
||||||
HangingProtocolService.addProtocols(protocols)
|
});
|
||||||
})
|
|
||||||
|
|
||||||
const setupRouteInit = async () => {
|
const setupRouteInit = async () => {
|
||||||
if (route.init) {
|
if (route.init) {
|
||||||
return await route.init(/**...**/)
|
return await route.init(/**...**/);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await defaultRouteInit(/**...**/)
|
return await defaultRouteInit(/**...**/);
|
||||||
}
|
};
|
||||||
|
|
||||||
let unsubscriptions
|
let unsubscriptions;
|
||||||
setupRouteInit().then((unsubs) => {
|
setupRouteInit().then(unsubs => {
|
||||||
unsubscriptions = unsubs
|
unsubscriptions = unsubs;
|
||||||
})
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
extensionManager.onModeExit()
|
extensionManager.onModeExit();
|
||||||
mode?.onModeExit({ servicesManager, extensionManager })
|
mode?.onModeExit({ servicesManager, extensionManager });
|
||||||
unsubscriptions.forEach((unsub) => {
|
unsubscriptions.forEach(unsub => {
|
||||||
unsub()
|
unsub();
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
},
|
});
|
||||||
)
|
return <> /**...**/ </>;
|
||||||
return <> /**...**/ </>
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -49,7 +49,7 @@ async function defaultRouteInit({
|
|||||||
unsubscriptions.push(seriesAddedUnsubscribe);
|
unsubscriptions.push(seriesAddedUnsubscribe);
|
||||||
|
|
||||||
studyInstanceUIDs.forEach(StudyInstanceUID => {
|
studyInstanceUIDs.forEach(StudyInstanceUID => {
|
||||||
dataSource.retrieveSeriesMetadata({ StudyInstanceUID });
|
dataSource.retrieve.series.metadata({ StudyInstanceUID });
|
||||||
});
|
});
|
||||||
|
|
||||||
return unsubscriptions;
|
return unsubscriptions;
|
||||||
@ -216,7 +216,9 @@ export default function ModeRoute({
|
|||||||
// Adding hanging protocols of extensions after onModeEnter since
|
// Adding hanging protocols of extensions after onModeEnter since
|
||||||
// it will reset the protocols
|
// it will reset the protocols
|
||||||
hangingProtocols.forEach(extentionProtocols => {
|
hangingProtocols.forEach(extentionProtocols => {
|
||||||
const hangingProtocolModule = extensionManager.getModuleEntry(extentionProtocols);
|
const hangingProtocolModule = extensionManager.getModuleEntry(
|
||||||
|
extentionProtocols
|
||||||
|
);
|
||||||
if (hangingProtocolModule?.protocols) {
|
if (hangingProtocolModule?.protocols) {
|
||||||
HangingProtocolService.addProtocols(hangingProtocolModule.protocols);
|
HangingProtocolService.addProtocols(hangingProtocolModule.protocols);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user