Skip to content

Commit

Permalink
Now providing the ability to browse remote DICOMWeb servers
Browse files Browse the repository at this point in the history
  • Loading branch information
amazy committed Jul 25, 2024
1 parent d697e19 commit 43265c5
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 26 deletions.
22 changes: 20 additions & 2 deletions WebApplication/src/components/ResourceButtonGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,26 @@ export default {
const jobId = await api.remoteDicomRetrieveResource(this.capitalizeFirstLetter(this.resourceLevel), this.studiesRemoteSource, moveQuery, this.system.DicomAet);
this.$store.dispatch('jobs/addJob', { jobId: jobId, name: 'Retrieve ' + this.capitalizeFirstLetter(this.resourceLevel) + ' from (' + this.studiesRemoteSource + ')'});
}
} else {
console.log("TODO");
} else if (this.studiesSourceType == SourceType.REMOTE_DICOM_WEB) {
let resources;
if (this.resourceLevel == "bulk") {
resources = this.selectedStudies.map(s => { return {"Study": s['ID']}});
} else if (this.resourceLevel == "study") {
resources = [{"Study": this.studyMainDicomTags.StudyInstanceUID}];
} else if (this.resourceLevel == "series") {
resources = [{
"Study": this.studyMainDicomTags.StudyInstanceUID,
"Series": this.seriesMainDicomTags.SeriesInstanceUID
}];
} else if (this.resourceLevel == "instance") {
resources = [{
"Study": this.studyMainDicomTags.StudyInstanceUID,
"Series": this.seriesMainDicomTags.SeriesInstanceUID,
"Instance": this.instanceTags.SOPInstanceUID
}];
}
const jobId = await api.wadoRsRetrieve(this.studiesRemoteSource, resources);
this.$store.dispatch('jobs/addJob', { jobId: jobId, name: 'Retrieve ' + this.capitalizeFirstLetter(this.resourceLevel) + ' from (' + this.studiesRemoteSource + ')'});
}
},
capitalizeFirstLetter(level) {
Expand Down
14 changes: 14 additions & 0 deletions WebApplication/src/components/SeriesDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ export default {
"MainDicomTags": s
}})
this.seriesInstances = this.seriesInstances.sort((a, b) => (parseInt(a.MainDicomTags.InstanceNumber) ?? a.MainDicomTags.SOPInstanceUID) < (parseInt(b.MainDicomTags.InstanceNumber) ?? b.MainDicomTags.SOPInstanceUID) ? 1 : -1);
} else if (this.studiesSourceType == SourceType.REMOTE_DICOM_WEB) {
let remoteInstances = (await api.qidoRs("Instance", this.studiesRemoteSource, {
"StudyInstanceUID": this.studyMainDicomTags.StudyInstanceUID,
"SeriesInstanceUID": this.seriesMainDicomTags.SeriesInstanceUID,
"SOPInstanceUID": "",
"InstanceNumber": "",
"NumberOfFrames": ""
},
false /* isUnique */));
this.seriesInstances = remoteInstances.map(s => { return {
"ID": s["SOPInstanceUID"],
"MainDicomTags": s
}})
this.seriesInstances = this.seriesInstances.sort((a, b) => (parseInt(a.MainDicomTags.InstanceNumber) ?? a.MainDicomTags.SOPInstanceUID) < (parseInt(b.MainDicomTags.InstanceNumber) ?? b.MainDicomTags.SOPInstanceUID) ? 1 : -1);
}
},
components: { ResourceButtonGroup, InstanceList, ResourceDetailText },
Expand Down
4 changes: 1 addition & 3 deletions WebApplication/src/components/SeriesItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ export default {
instancesCount() {
if (this.studiesSourceType == SourceType.LOCAL_ORTHANC) {
return this.seriesInfo.Instances.length;
} else if (this.studiesSourceType == SourceType.REMOTE_DICOM) {
} else if (this.studiesSourceType == SourceType.REMOTE_DICOM || this.studiesSourceType == SourceType.REMOTE_DICOM_WEB) {
return this.seriesInfo.MainDicomTags.NumberOfSeriesRelatedInstances;
} else {
console.log("TODO");
}
}
},
Expand Down
22 changes: 14 additions & 8 deletions WebApplication/src/components/SideBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default {
}
},
hasQueryableDicomWebServers() {
return false; // TODO this.queryableDicomWebServers.length > 0;
return this.queryableDicomWebServers.length > 0;
},
hasQueryableDicomModalities() {
return this.uiOptions.EnableDicomModalities && this.queryableDicomModalities.length > 0;
Expand All @@ -65,20 +65,23 @@ export default {
return this.userProfile != null && this.userProfile.name;
},
displayedStudyCount() {
return this.studiesIds.length;
if (this.studiesSourceType == SourceType.LOCAL_ORTHANC) {
return this.studiesIds.length;
} else {
return "-";
}
},
orthancApiUrl() {
return orthancApiUrl;
},
},
methods: {
// selectModality(modality) {
// this.selectedModality = modality;
// },
isSelectedModality(modality) {
// return this.selectedModality === modality;
return this.studiesSourceType == SourceType.REMOTE_DICOM && this.studiesRemoteSource == modality;
},
isSelectedDicomWebServer(server) {
return this.studiesSourceType == SourceType.REMOTE_DICOM_WEB && this.studiesRemoteSource == server;
},
isEchoRunning(modality) {
return this.modalitiesEchoStatus[modality] == null;
},
Expand Down Expand Up @@ -205,8 +208,11 @@ export default {
<span class="arrow ms-auto"></span>
</li>
<ul class="sub-menu collapse" id="dicomweb-servers-list">
<li v-for="server in queryableDicomWebServers" :key="server" class="active">
<a href="#">{{ server }} (TODO)</a>
<li v-for="server in queryableDicomWebServers" :key="server" v-bind:class="{ 'active': this.isSelectedDicomWebServer(server) }">
<router-link class="router-link"
:to="{ path: '/filtered-studies', query: { 'source-type': 'dicom-web', 'remote-source': server } }">
{{ server }}
</router-link>
</li>
</ul>

Expand Down
15 changes: 14 additions & 1 deletion WebApplication/src/components/StudyDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export default {
async reloadSeriesList() {
if (this.studiesSourceType == SourceType.LOCAL_ORTHANC) {
this.studySeries = (await api.getStudySeries(this.studyId));
} else {
} else if (this.studiesSourceType == SourceType.REMOTE_DICOM) {
let remoteSeries = (await api.remoteDicomFind("Series", this.studiesRemoteSource, {
"StudyInstanceUID": this.studyMainDicomTags.StudyInstanceUID,
"PatientID": this.patientMainDicomTags.PatientID,
Expand All @@ -93,6 +93,19 @@ export default {
"ID": s["SeriesInstanceUID"],
"MainDicomTags": s
}})
} else if (this.studiesSourceType == SourceType.REMOTE_DICOM_WEB) {
let remoteSeries = (await api.qidoRs("Series", this.studiesRemoteSource, {
"StudyInstanceUID": this.studyMainDicomTags.StudyInstanceUID,
"NumberOfSeriesRelatedInstances": "",
"Modality": "",
"SeriesDescription": "",
"SeriesNumber": ""
},
false /* isUnique */));
this.studySeries = remoteSeries.map(s => { return {
"ID": s["SeriesInstanceUID"],
"MainDicomTags": s
}})
}
},
async labelsUpdated() {
Expand Down
10 changes: 6 additions & 4 deletions WebApplication/src/components/StudyItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ export default {
allLabels: state => state.labels.allLabels
}),
modalitiesInStudyForDisplay() {
return this.study.RequestedTags.ModalitiesInStudy.split('\\').join(',');
if (this.study.RequestedTags.ModalitiesInStudy) {
return this.study.RequestedTags.ModalitiesInStudy.split('\\').join(',');
} else {
return "";
}
},
showLabels() {
if (this.studiesSourceType == SourceType.LOCAL_ORTHANC) {
Expand All @@ -110,10 +114,8 @@ export default {
seriesCount() {
if (this.study.sourceType == SourceType.LOCAL_ORTHANC) {
return this.study.Series.length;
} else if (this.study.sourceType == SourceType.REMOTE_DICOM) {
} else if (this.study.sourceType == SourceType.REMOTE_DICOM || this.study.sourceType == SourceType.REMOTE_DICOM_WEB) {
return this.study.MainDicomTags.NumberOfStudyRelatedSeries;
} else {
console.log("TODO");
}
}
},
Expand Down
1 change: 1 addition & 0 deletions WebApplication/src/components/StudyList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ export default {
//console.log("StudyList: updateFilterFromRoute", this.updatingFilterUi, filters);
this.updatingFilterUi = true;
await this.$store.dispatch('studies/clearStudies');
await this.$store.dispatch('studies/clearFilterNoReload');
var keyValueFilters = {};
Expand Down
74 changes: 69 additions & 5 deletions WebApplication/src/orthancApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,21 +156,21 @@ export default {
return response.data['ID'];
},
async cancelRemoteDicomFind() {
if (window.axioRemoteDicomFindAbortController) {
window.axioRemoteDicomFindAbortController.abort();
window.axioRemoteDicomFindAbortController = null;
if (window.axiosRemoteDicomFindAbortController) {
window.axiosRemoteDicomFindAbortController.abort();
window.axiosRemoteDicomFindAbortController = null;
}
},
async remoteDicomFind(level, remoteModality, filterQuery, isUnique) {
if (isUnique) {
await this.cancelRemoteDicomFind();
window.axioRemoteDicomFindAbortController = new AbortController();
window.axiosRemoteDicomFindAbortController = new AbortController();
}

try {
let axiosOptions = {}
if (isUnique) {
axiosOptions['signal'] = window.axioRemoteDicomFindAbortController.signal
axiosOptions['signal'] = window.axiosRemoteDicomFindAbortController.signal
}
const queryResponse = (await axios.post(orthancApiUrl + "modalities/" + remoteModality + "/query", {
"Level": level,
Expand All @@ -188,6 +188,70 @@ export default {
return {};
}
},
async qidoRs(level, remoteServer, filterQuery, isUnique){
if (isUnique) {
await this.cancelQidoRs();
window.axiosQidoRsAbortController = new AbortController();
}

try {
let axiosOptions = {}
if (isUnique) {
axiosOptions['signal'] = window.axiosQidoRsAbortController.signal
}
let uri = null;
if (level == "Study") {
uri = "/studies";
} else if (level == "Series") {
uri = "/studies/" + filterQuery["StudyInstanceUID"] + "/series";
delete filterQuery["StudyInstanceUID"]; // we don't need it the filter since it is in the url
} else if (level == "Instance") {
uri = "/studies/" + filterQuery["StudyInstanceUID"] + "/series/" + filterQuery["SeriesInstanceUID"] + "/instances";
delete filterQuery["StudyInstanceUID"]; // we don't need it the filter since it is in the url
delete filterQuery["SeriesInstanceUID"];
}
let args = {...filterQuery};
args["limit"] = String(store.state.configuration.uiOptions.MaxStudiesDisplayed);
args["fuzzymatching"] = "true";

const queryResponse = (await axios.post(orthancApiUrl + "dicom-web/servers/" + remoteServer + "/qido", {
"Uri": uri,
"Arguments": args
},
axiosOptions
)).data;
// transform the response into something similar to a DICOM C-Find API response
let responses = [];
for (let qr of queryResponse) {
let r = {}
for (const [k, v] of Object.entries(qr)) {
if (v.Value) {
r[v.Name] = v.Value;
}
}
responses.push(r);
}
return responses;
} catch (err)
{
console.log("Error during query:", err); // TODO: display error to user
return {};
}
},
async cancelQidoRs() {
if (window.axiosQidoRsAbortController) {
window.axiosQidoRsAbortController.abort();
window.axiosQidoRsAbortController = null;
}
},
async wadoRsRetrieve(remoteServer, resources){
const retrieveJob = (await axios.post(orthancApiUrl + "dicom-web/servers/" + remoteServer + "/retrieve", {
"Resources": resources,
"Asynchronous": true
}
)).data;
return retrieveJob["ID"];
},
async remoteDicomRetrieveResource(level, remoteModality, filterQuery, targetAet) {
const response = (await axios.post(orthancApiUrl + "modalities/" + remoteModality + "/move", {
"Level": level,
Expand Down
11 changes: 9 additions & 2 deletions WebApplication/src/store/modules/studies.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ const actions = {
if (state.sourceType == SourceType.LOCAL_ORTHANC) {
studies = (await api.findStudies(getters.filterQuery, state.labelsFilter, "All"));
studies.sort((a, b) => (a.MainDicomTags.StudyDate ?? "") < (b.MainDicomTags.StudyDate ?? "") ? 1 : -1);
} else if (state.sourceType == SourceType.REMOTE_DICOM) {
} else if (state.sourceType == SourceType.REMOTE_DICOM || state.sourceType == SourceType.REMOTE_DICOM_WEB) {
// make sure to fill all columns of the StudyList
let filters = {
"PatientBirthDate": "",
Expand All @@ -230,7 +230,14 @@ const actions = {
for (const [k, v] of Object.entries(getters.filterQuery)) {
filters[k] = v;
}
let remoteStudies = (await api.remoteDicomFind("Study", state.remoteSource, filters, true /* isUnique */));

let remoteStudies;
if (state.sourceType == SourceType.REMOTE_DICOM) {
remoteStudies = (await api.remoteDicomFind("Study", state.remoteSource, filters, true /* isUnique */));
} else if (state.sourceType == SourceType.REMOTE_DICOM_WEB) {
remoteStudies = (await api.qidoRs("Study", state.remoteSource, filters, true /* isUnique */));
}

// copy the tags in MainDicomTags, ... to have a common study structure between local and remote studies
studies = remoteStudies.map(s => { return {"MainDicomTags": s, "PatientMainDicomTags": s, "RequestedTags": s, "ID": s["StudyInstanceUID"]} });
}
Expand Down
3 changes: 2 additions & 1 deletion release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ Pending changes in the mainline
===============================

Changes:
- Refactored the Remote Study List when browsing DICOM Modalities. It is now identical to the main
- Refactored the Remote Study List when browsing remote DICOM Modalities. It is now identical to the main
local study list with a reduced list of actions (only the retrieve action is available).
- the /ui/app/#/filtered-remote-studies has been replaced by /ui/app/#/filtered-studies?source-type=dicom&remote-source=...
- Now providing the ability to browse remote DICOMWeb servers.
- Authorization tokens can be provided in the URL as query args e.g: http://localhost:8042/ui/app/?token=my-token
or /ui/app/filtered-studies?StudyDescription=PET&token=my-token and these tokens will be included as HTTP headers
in all requests issued by OE2. Note that the query args must be positioned before the '#' in the URL.
Expand Down

0 comments on commit 43265c5

Please sign in to comment.