Mercurial > hg > orthanc-dicomweb
changeset 307:5a9238e974ea refactoring
first version of the DICOMweb client
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Tue, 18 Jun 2019 18:38:30 +0200 |
parents | 4a0b759019ac |
children | 78c9a7d207b8 |
files | CMakeLists.txt Plugin/Plugin.cpp Resources/CMake/JavaScriptLibraries.cmake WebApplication/app.js WebApplication/index.html |
diffstat | 5 files changed, 781 insertions(+), 1 deletions(-) [+] |
line wrap: on
line diff
--- a/CMakeLists.txt Tue Jun 18 15:34:29 2019 +0200 +++ b/CMakeLists.txt Tue Jun 18 18:38:30 2019 +0200 @@ -45,6 +45,10 @@ set(ORTHANC_SDK_VERSION "1.5.4" CACHE STRING "Version of the Orthanc plugin SDK to use, if not using the system version (can be \"1.5.4\", or \"framework\")") +set(BUILD_BOOTSTRAP_VUE ON CACHE BOOL "Compile Bootstrap-Vue from sources") +set(BUILD_BABEL_POLYFILL ON CACHE BOOL "Retrieve babel-polyfill from npm") + + # Download and setup the Orthanc framework include(${CMAKE_SOURCE_DIR}/Resources/Orthanc/DownloadOrthancFramework.cmake) @@ -61,6 +65,7 @@ include_directories(${ORTHANC_ROOT}) include(${CMAKE_SOURCE_DIR}/Resources/CMake/GdcmConfiguration.cmake) +include(${CMAKE_SOURCE_DIR}/Resources/CMake/JavaScriptLibraries.cmake) if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK) @@ -109,11 +114,18 @@ endif() +EmbedResources( + --no-upcase-check + JAVASCRIPT_LIBS ${JAVASCRIPT_LIBS_DIR} + ) + + include_directories(${ORTHANC_ROOT}/Core) # To access "OrthancException.h" add_definitions( -DHAS_ORTHANC_EXCEPTION=1 -DORTHANC_ENABLE_LOGGING_PLUGIN=1 + -DDICOMWEB_CLIENT_PATH="${CMAKE_SOURCE_DIR}/WebApplication/" ) set(CORE_SOURCES
--- a/Plugin/Plugin.cpp Tue Jun 18 15:34:29 2019 +0200 +++ b/Plugin/Plugin.cpp Tue Jun 18 18:38:30 2019 +0200 @@ -18,7 +18,6 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. **/ - #include "DicomWebClient.h" #include "DicomWebServers.h" #include "GdcmParsedDicomFile.h" @@ -28,8 +27,11 @@ #include "WadoUri.h" #include <Plugins/Samples/Common/OrthancPluginCppWrapper.h> +#include <Core/SystemToolbox.h> #include <Core/Toolbox.h> +#include <EmbeddedResources.h> + bool RequestHasKey(const OrthancPluginHttpRequest* request, const char* key) { @@ -529,6 +531,54 @@ } +template <enum Orthanc::EmbeddedResources::DirectoryResourceId folder> +void ServeEmbeddedFolder(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + std::string path = "/" + std::string(request->groups[0]); + const char* mime = Orthanc::EnumerationToString(Orthanc::SystemToolbox::AutodetectMimeType(path)); + + std::string s; + Orthanc::EmbeddedResources::GetDirectoryResource(s, folder, path.c_str()); + + const char* resource = s.size() ? s.c_str() : NULL; + OrthancPluginAnswerBuffer(context, output, resource, s.size(), mime); + } +} + + +void ServeDicomWebClient(OrthancPluginRestOutput* output, + const char* url, + const OrthancPluginHttpRequest* request) +{ + OrthancPluginContext* context = OrthancPlugins::GetGlobalContext(); + + if (request->method != OrthancPluginHttpMethod_Get) + { + OrthancPluginSendMethodNotAllowed(context, output, "GET"); + } + else + { + const std::string path = std::string(DICOMWEB_CLIENT_PATH) + std::string(request->groups[0]); + const char* mime = Orthanc::EnumerationToString(Orthanc::SystemToolbox::AutodetectMimeType(path)); + + OrthancPlugins::MemoryBuffer f; + f.ReadFile(path); + + OrthancPluginAnswerBuffer(context, output, f.GetData(), f.GetSize(), mime); + } +} + + extern "C" { ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context) @@ -599,6 +649,15 @@ OrthancPlugins::RegisterRestCallback<RetrieveFromServer>(root + "servers/([^/]*)/retrieve", true); OrthancPlugins::RegisterRestCallback<QidoClient>(root + "servers/([^/]*)/qido", true); OrthancPlugins::RegisterRestCallback<DeleteClient>(root + "servers/([^/]*)/delete", true); + + OrthancPlugins::RegisterRestCallback + <ServeEmbeddedFolder<Orthanc::EmbeddedResources::JAVASCRIPT_LIBS> > + (root + "app/libs/(.*)", true); + + OrthancPlugins::RegisterRestCallback<ServeDicomWebClient>(root + "app/client/(.*)", true); + + std::string uri = root + "app/client/index.html"; + OrthancPluginSetRootUri(context, uri.c_str()); } else {
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Resources/CMake/JavaScriptLibraries.cmake Tue Jun 18 18:38:30 2019 +0200 @@ -0,0 +1,173 @@ +# Orthanc - A Lightweight, RESTful DICOM Store +# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics +# Department, University Hospital of Liege, Belgium +# Copyright (C) 2017-2019 Osimis S.A., Belgium +# +# This program is free software: you can redistribute it and/or +# modify it under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation, either version 3 of +# the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + +set(BASE_URL "http://orthanc.osimis.io/ThirdPartyDownloads/dicom-web") + +DownloadPackage( + "da0189f7c33bf9f652ea65401e0a3dc9" + "${BASE_URL}/bootstrap-4.3.1.zip" + "${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1") + +DownloadPackage( + "8242afdc5bd44105d9dc9e6535315484" + "${BASE_URL}/vuejs-2.6.10.tar.gz" + "${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10") + +DownloadPackage( + "3e2b4e1522661f7fcf8ad49cb933296c" + "${BASE_URL}/axios-0.19.0.tar.gz" + "${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0") + +DownloadPackage( + "a6145901f233f7d54165d8ade779082e" + "${BASE_URL}/Font-Awesome-4.7.0.tar.gz" + "${CMAKE_CURRENT_BINARY_DIR}/Font-Awesome-4.7.0") + + +set(BOOTSTRAP_VUE_SOURCES_DIR ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-vue-2.0.0-rc.24) + +if (BUILD_BOOTSTRAP_VUE OR + BUILD_BABEL_POLYFILL) + find_program(NPM_EXECUTABLE npm) + if (${NPM_EXECUTABLE} MATCHES "NPM_EXECUTABLE-NOTFOUND") + message(FATAL_ERROR "Please install the 'npm' standard command-line tool") + endif() +endif() + +if (BUILD_BOOTSTRAP_VUE) + DownloadPackage( + "36ab31495ab94162e159619532e8def5" + "${BASE_URL}/bootstrap-vue-2.0.0-rc.24.tar.gz" + "${BOOTSTRAP_VUE_SOURCES_DIR}") + + if (NOT IS_DIRECTORY "${BOOTSTRAP_VUE_SOURCES_DIR}/node_modules") + execute_process( + COMMAND ${NPM_EXECUTABLE} install + WORKING_DIRECTORY ${BOOTSTRAP_VUE_SOURCES_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + + if (Failure) + message(FATAL_ERROR "Error while running 'npm install' on Bootstrap-Vue") + endif() + endif() + + if (NOT IS_DIRECTORY "${BOOTSTRAP_VUE_SOURCES_DIR}/dist") + execute_process( + COMMAND ${NPM_EXECUTABLE} run build + WORKING_DIRECTORY ${BOOTSTRAP_VUE_SOURCES_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + + if (Failure) + message(FATAL_ERROR "Error while running 'npm build' on Bootstrap-Vue") + endif() + endif() + +else() + + ## + ## Generation of the precompiled Bootstrap-Vue package: + ## + ## Possibility 1 (build from sources): + ## $ cmake -DBUILD_BOOTSTRAP_VUE=ON . + ## $ tar cvfz bootstrap-vue-2.0.0-rc.24-dist.tar.gz bootstrap-vue-2.0.0-rc.24/dist/ + ## + ## Possibility 2 (download from CDN): + ## $ mkdir /tmp/i && cd /tmp/i + ## $ wget -r --no-parent https://unpkg.com/bootstrap-vue@2.0.0-rc.24/dist/ + ## $ mv unpkg.com/bootstrap-vue@2.0.0-rc.24/ bootstrap-vue-2.0.0-rc.24 + ## $ rm bootstrap-vue-2.0.0-rc.24/dist/index.html + ## $ tar cvfz bootstrap-vue-2.0.0-rc.24-dist.tar.gz bootstrap-vue-2.0.0-rc.24/dist/ + + DownloadPackage( + "ba0e67b1f0b4ce64e072b42b17f6c578" + "${BASE_URL}/bootstrap-vue-2.0.0-rc.24-dist.tar.gz" + "${BOOTSTRAP_VUE_SOURCES_DIR}") + +endif() + + +if (BUILD_BABEL_POLYFILL) + set(BABEL_POLYFILL_SOURCES_DIR ${CMAKE_CURRENT_BINARY_DIR}/node_modules/babel-polyfill/dist) + + if (NOT IS_DIRECTORY "${BABEL_POLYFILL_SOURCES_DIR}") + execute_process( + COMMAND ${NPM_EXECUTABLE} install babel-polyfill + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + RESULT_VARIABLE Failure + OUTPUT_QUIET + ) + + if (Failure) + message(FATAL_ERROR "Error while running 'npm install' on Bootstrap-Vue") + endif() + endif() +else() + + ## curl -L https://unpkg.com/babel-polyfill@6.26.0/dist/polyfill.min.js | gzip > babel-polyfill-6.26.0.min.js.gz + + set(BABEL_POLYFILL_SOURCES_DIR ${CMAKE_CURRENT_BINARY_DIR}) + DownloadCompressedFile( + "49f7bad4176d715ce145e75c903988ef" + "${BASE_URL}/babel-polyfill-6.26.0.min.js.gz" + "${CMAKE_CURRENT_BINARY_DIR}/polyfill.min.js") + +endif() + + +set(JAVASCRIPT_LIBS_DIR ${CMAKE_CURRENT_BINARY_DIR}/javascript-libs) +file(MAKE_DIRECTORY ${JAVASCRIPT_LIBS_DIR}) + +file(COPY + ${BABEL_POLYFILL_SOURCES_DIR}/polyfill.min.js + ${BOOTSTRAP_VUE_SOURCES_DIR}/dist/bootstrap-vue.min.js + ${CMAKE_CURRENT_BINARY_DIR}/axios-0.19.0/dist/axios.min.js + ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/js/bootstrap.min.js + ${CMAKE_CURRENT_BINARY_DIR}/vue-2.6.10/dist/vue.min.js + DESTINATION + ${JAVASCRIPT_LIBS_DIR}/js + ) + +file(COPY + ${BOOTSTRAP_VUE_SOURCES_DIR}/dist/bootstrap-vue.min.css + ${CMAKE_CURRENT_BINARY_DIR}/Font-Awesome-4.7.0/css/font-awesome.min.css + ${CMAKE_CURRENT_BINARY_DIR}/bootstrap-4.3.1/dist/css/bootstrap.min.css + DESTINATION + ${JAVASCRIPT_LIBS_DIR}/css + ) + +file(COPY + ${CMAKE_CURRENT_BINARY_DIR}/Font-Awesome-4.7.0/fonts/FontAwesome.otf + ${CMAKE_CURRENT_BINARY_DIR}/Font-Awesome-4.7.0/fonts/fontawesome-webfont.eot + ${CMAKE_CURRENT_BINARY_DIR}/Font-Awesome-4.7.0/fonts/fontawesome-webfont.svg + ${CMAKE_CURRENT_BINARY_DIR}/Font-Awesome-4.7.0/fonts/fontawesome-webfont.ttf + ${CMAKE_CURRENT_BINARY_DIR}/Font-Awesome-4.7.0/fonts/fontawesome-webfont.woff + ${CMAKE_CURRENT_BINARY_DIR}/Font-Awesome-4.7.0/fonts/fontawesome-webfont.woff2 + DESTINATION + ${JAVASCRIPT_LIBS_DIR}/fonts + ) + +file(COPY + ${ORTHANC_ROOT}/Resources/OrthancLogo.png + DESTINATION + ${JAVASCRIPT_LIBS_DIR}/img + )
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebApplication/app.js Tue Jun 18 18:38:30 2019 +0200 @@ -0,0 +1,327 @@ +var DICOM_TAG_ACCESSION_NUMBER = '00080050'; +var DICOM_TAG_MODALITY = '00080060'; +var DICOM_TAG_PATIENT_ID = '00100020'; +var DICOM_TAG_PATIENT_NAME = '00100010'; +var DICOM_TAG_SERIES_DESCRIPTION = '0008103E'; +var DICOM_TAG_SERIES_INSTANCE_UID = '0020000E'; +var DICOM_TAG_SOP_INSTANCE_UID = '00080018'; +var DICOM_TAG_STUDY_DATE = '00080020'; +var DICOM_TAG_STUDY_ID = '00200010'; +var DICOM_TAG_STUDY_INSTANCE_UID = '0020000D'; +var DEFAULT_PREVIEW = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='; +var MAX_RESULTS = 100; + +var app = new Vue({ + el: '#app', + computed: { + studiesCount() { + return this.studies.length + }, + seriesCount() { + return this.series.length + } + }, + data: { + previewFailure: true, + preview: DEFAULT_PREVIEW, + showTruncatedStudies: false, + showNoServer: false, + showStudies: false, + showSeries: false, + maxResults: MAX_RESULTS, + currentPage: 0, + perPage: 10, + servers: [ ], + serversInfo: { }, + lookup: { }, + studies: [ ], + currentStudy: null, + studiesFields: [ + { + key: DICOM_TAG_PATIENT_ID + '.Value', + label: 'Patient ID', + sortable: true + }, + { + key: DICOM_TAG_PATIENT_NAME + '.Value', + label: 'Patient name', + sortable: true + }, + { + key: DICOM_TAG_ACCESSION_NUMBER + '.Value', + label: 'Accession number', + sortable: true + }, + { + key: DICOM_TAG_STUDY_DATE + '.Value', + label: 'Study date', + sortable: true + }, + { + key: 'operations', + label: '' + } + ], + studyToDelete: null, + studyTags: [ ], + studyTagsFields: [ + { + key: 'Tag', + sortable: true + }, + { + key: 'Name', + label: 'Description', + sortable: true + }, + { + key: 'Value', + sortable: true + } + ], + series: [ ], + seriesFields: [ + { + key: DICOM_TAG_SERIES_DESCRIPTION + '.Value', + label: 'Series description', + sortable: true + }, + { + key: DICOM_TAG_MODALITY + '.Value', + label: 'Modality', + sortable: true + }, + { + key: 'operations', + label: '' + } + ], + seriesToDelete: null, + seriesTags: [ ], + seriesTagsFields: [ + { + key: 'Tag', + sortable: true + }, + { + key: 'Name', + label: 'Description', + sortable: true + }, + { + key: 'Value', + sortable: true + } + ], + scrollToSeries: false, + scrollToStudies: false + }, + mounted: () => { + axios + .get('../../servers?expand') + .then(response => { + app.serversInfo = response.data; + app.servers = Object.keys(response.data).map(i => i); + app.Clear(); + }) + }, + methods: { + ScrollToRef: function(refName) { + var element = app.$refs[refName]; + window.scrollTo(0, element.offsetTop + 200); + }, + + /** + * Studies + **/ + + SetStudies: function(response) { + if (response.data.length > app.maxResults) { + app.showTruncatedStudies = true; + app.studies = response.data.splice(0, app.maxResults); + } else { + app.showTruncatedStudies = false; + app.studies = response.data; + } + app.showStudies = true; + app.showSeries = false; + app.studyToDelete = null; + app.scrollToStudies = true; + }, + ExecuteLookup: function() { + var args = { + 'fuzzymatching' : 'true', + 'limit' : (app.maxResults + 1).toString() + }; + + if ('patientName' in app.lookup) { + args[DICOM_TAG_PATIENT_NAME] = app.lookup.patientName; + } + + if ('patientID' in app.lookup) { + args[DICOM_TAG_PATIENT_ID] = app.lookup.patientID; + } + + if ('studyDate' in app.lookup) { + args[DICOM_TAG_STUDY_DATE] = app.lookup.studyDate; + } + + if ('accessionNumber' in app.lookup) { + args[DICOM_TAG_ACCESSION_NUMBER] = app.lookup.accessionNumber; + } + + axios + .post('../../servers/' + app.lookup.server + '/qido', { + 'Uri' : '/studies', + 'Arguments' : args, + }) + .then(app.SetStudies); + }, + Clear: function() { + app.lookup = {}; + currentStudy = null; + app.showSeries = false; + app.showStudies = false; + if (app.servers.length == 0) { + app.showNoServer = true; + } else { + app.showNoServer = false; + app.lookup.server = app.servers[0]; + } + }, + OnAllStudies: function (event) { + event.preventDefault(); + axios + .post('../../servers/' + app.lookup.server + '/qido', { + 'Uri' : '/studies', + 'Arguments' : { 'limit' : (app.maxResults + 1).toString() } + }) + .then(app.SetStudies); + }, + OnLookup: function(event) { + event.preventDefault(); + app.ExecuteLookup(); + }, + OnReset: function(event) { + event.preventDefault(); + app.Clear(); + }, + OpenStudyDetails: function(study) { + app.studyTags = Object.keys(study).map(i => { + var item = study[i]; + item['Tag'] = i; + return item; + }); + + app.$refs['study-details'].show(); + }, + ConfirmDeleteStudy: function(study) { + app.studyToDelete = study; + app.$bvModal.show('study-delete-confirm'); + }, + ExecuteDeleteStudy: function(study) { + axios + .post('../../servers/' + app.lookup.server + '/delete', { + 'Level': 'Study', + 'StudyInstanceUID': app.studyToDelete[DICOM_TAG_STUDY_INSTANCE_UID].Value + }) + .then(app.ExecuteLookup); + }, + + + /** + * Series + **/ + + LoadSeriesOfCurrentStudy: function() { + axios + .post('../../servers/' + app.lookup.server + '/qido', { + 'Uri' : '/studies/' + app.currentStudy + '/series' + }) + .then(response => { + app.series = response.data; + app.showSeries = true; + app.seriesToDelete = null; + app.scrollToSeries = true; + }) + .catch(app.ExecuteLookup); // Parent study was deleted + }, + OpenSeries: function(series) { + app.currentStudy = series[DICOM_TAG_STUDY_INSTANCE_UID].Value; + app.LoadSeriesOfCurrentStudy(); + }, + OpenSeriesDetails: function(series) { + app.seriesTags = Object.keys(series).map(i => { + var item = series[i]; + item['Tag'] = i; + return item; + }); + + app.$refs['series-details'].show(); + }, + OpenSeriesPreview: function(series) { + axios + .post('../../servers/' + app.lookup.server + '/get', { + 'Uri' : ('/studies/' + app.currentStudy + '/series/' + + series[DICOM_TAG_SERIES_INSTANCE_UID].Value + '/instances') + }) + .then(response => { + var instance = response.data[Math.floor(response.data.length / 2)]; + + axios + .post('../../servers/' + app.lookup.server + '/get', { + 'Uri' : ('/studies/' + app.currentStudy + '/series/' + + series[DICOM_TAG_SERIES_INSTANCE_UID].Value + '/instances/' + + instance[DICOM_TAG_SOP_INSTANCE_UID].Value + '/rendered') + }, { + responseType: 'arraybuffer' + }) + .then(response => { + // https://github.com/axios/axios/issues/513 + var image = btoa(new Uint8Array(response.data) + .reduce((data, byte) => data + String.fromCharCode(byte), '')); + app.preview = ("data:" + + response.headers['content-type'].toLowerCase() + + ";base64," + image); + app.previewFailure = false; + }) + .catch(response => { + app.previewFailure = true; + }) + .finally(function() { + app.$refs['series-preview'].show(); + }) + }); + }, + ConfirmDeleteSeries: function(series) { + app.seriesToDelete = series; + app.$bvModal.show('series-delete-confirm'); + }, + ExecuteDeleteSeries: function(series) { + axios + .post('../../servers/' + app.lookup.server + '/delete', { + 'Level': 'Series', + 'StudyInstanceUID': app.currentStudy, + 'SeriesInstanceUID': app.seriesToDelete[DICOM_TAG_SERIES_INSTANCE_UID].Value + }) + .then(app.LoadSeriesOfCurrentStudy); + } + }, + + updated: function () { + this.$nextTick(function () { + // Code that will run only after the + // entire view has been re-rendered + + if (app.scrollToStudies) { + app.scrollToStudies = false; + app.ScrollToRef('studies-top'); + } + + if (app.scrollToSeries) { + app.scrollToSeries = false; + app.ScrollToRef('series-top'); + } + }) + } +});
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/WebApplication/index.html Tue Jun 18 18:38:30 2019 +0200 @@ -0,0 +1,209 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + + <title>Orthanc - DICOMweb client</title> + + <!-- Add Bootstrap and Bootstrap-Vue CSS to the <head> section --> + <link type="text/css" rel="stylesheet" href="../libs/css/bootstrap.min.css"/> + <link type="text/css" rel="stylesheet" href="../libs/css/bootstrap-vue.min.css"/> + <link type="text/css" rel="stylesheet" href="../libs/css/font-awesome.min.css"/> + + <script src="../libs/js/polyfill.min.js"></script> + + <!-- CSS style to truncate long text in tables, provided they have + class "table-layout:fixed;" or attribute ":fixed=true" --> + <style> + table td { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + </style> + + </head> + <body> + <div class="container" id="app"> + <p style="height:1em"></p> + + <div class="jumbotron"> + <div class="row"> + <div class="col-sm-8"> + <h1 class="display-4">DICOMweb client</h1> + <p class="lead"> + This is a simple interface to the DICOMweb servers that are + configured in Orthanc. + </p> + <p> + <a class="btn btn-primary btn-lg" + href="https://book.orthanc-server.com/plugins/dicomweb.html" + target="_blank" role="button">Open documentation</a> + </p> + </div> + <div class="col-sm-4"> + <img class="img-fluid" alt="Orthanc" src="../libs/img/OrthancLogo.png" /> + </div> + </div> + </div> + + + + <!-- LOOKUP --> + + <div class="row"> + <b-alert variant="danger" dismissible v-model="showNoServer"> + No DICOMweb server is configured! + </b-alert> + <b-form style="width:100%;padding:5px;"> + <b-form-group label="DICOMweb server:" label-cols-sm="4" label-cols-lg="3"> + <b-form-select v-model="lookup.server" :options="servers"></b-form-select> + </b-form-group> + <b-form-group label="Patient ID:" label-cols-sm="4" label-cols-lg="3"> + <b-form-input v-model="lookup.patientID"></b-form-input> + </b-form-group> + <b-form-group label="Patient name:" label-cols-sm="4" label-cols-lg="3"> + <b-form-input v-model="lookup.patientName"></b-form-input> + </b-form-group> + <b-form-group label="Accession number:" label-cols-sm="4" label-cols-lg="3"> + <b-form-input v-model="lookup.accessionNumber"></b-form-input> + </b-form-group> + <b-form-group label="Study date:" label-cols-sm="4" label-cols-lg="3"> + <b-form-input v-model="lookup.studyDate"></b-form-input> + </b-form-group> + <p class="pull-right"> + <b-button type="submit" variant="success" @click="OnLookup" + size="lg">Do lookup</b-button> + <b-button type="success" variant="primary" @click="OnAllStudies" + size="lg">All studies</b-button> + <b-button type="reset" variant="outline-danger" @click="OnReset" + size="lg">Reset</b-button> + </p> + </b-form> + </div> + <hr/> + + + <!-- STUDIES --> + + <div ref="studies-top"></div> + + <div class="row" v-show="showStudies"> + <h1>Studies</h1> + </div> + <div class="row" v-show="showStudies"> + <b-alert variant="warning" dismissible v-model="showTruncatedStudies"> + More than {{ maxResults }} matching studies, results have been truncated! + </b-alert> + </div> + <div class="row" v-show="showStudies"> + <b-pagination v-model="currentPage" :per-page="perPage" :total-rows="studiesCount"></b-pagination> + <b-table striped hover :current-page="currentPage" :per-page="perPage" + :items="studies" :fields="studiesFields" :fixed="false"> + <template slot="operations" slot-scope="data"> + <b-button @click="OpenSeries(data.item)" title="Open series"><i class="fa fa-folder-open"></i></b-button> + <b-button @click="OpenStudyDetails(data.item)" title="Open tags"><i class="fa fa-address-card"></i></b-button> + <b-button title="Retrieve study using WADO-RS"><i class="fa fa-cloud-download"></i></b-button> + <b-button @click="ConfirmDeleteStudy(data.item)" + v-if="serversInfo[lookup.server].HasDelete" title="Delete remote study"> + <i class="fa fa-times"></i> + </b-button> + </template> + </b-table> + + <b-modal ref="study-details" size="xl" ok-only="true"> + <template slot="modal-title"> + Details of study + </template> + <div class="d-block text-center"> + <b-table striped :items="studyTags" :fields="studyTagsFields" :fixed="true"> + </b-table> + </div> + </b-modal> + + <b-modal id="study-delete-confirm" size="xl" @ok="ExecuteDeleteStudy"> + <template slot="modal-title"> + Confirm deletion + </template> + <div class="d-block"> + <p> + Are you sure you want to remove this study from the remote server? + </p> + <p> + Patient name: {{ studyToDelete && studyToDelete['00100010'] && studyToDelete['00100010'].Value }} + </p> + </div> + </b-modal> + </div> + + + <!-- SERIES --> + + <div ref="series-top"></div> + + <div class="row" v-show="showSeries"> + <h1>Series</h1> + </div> + <div class="row" v-show="showSeries"> + <b-pagination v-model="currentPage" :per-page="perPage" :total-rows="seriesCount"></b-pagination> + <b-table striped hover :current-page="currentPage" :per-page="perPage" + :items="series" :fields="seriesFields" :fixed="false"> + <template slot="operations" slot-scope="data"> + <b-button @click="OpenSeriesPreview(data.item)" title="Preview"><i class="fa fa-eye"></i></b-button> + <b-button @click="OpenSeriesDetails(data.item)" title="Open tags"><i class="fa fa-address-card"></i></b-button> + <b-button title="Retrieve series using WADO-RS"><i class="fa fa-cloud-download"></i></b-button> + <b-button @click="ConfirmDeleteSeries(data.item)" + v-if="serversInfo[lookup.server].HasDelete" title="Delete remote series"> + <i class="fa fa-times"></i> + </b-button> + </template> + </b-table> + + <b-modal ref="series-details" size="xl" ok-only="true"> + <template slot="modal-title"> + Details of series + </template> + <div class="d-block text-center"> + <b-table striped :items="seriesTags" :fields="seriesTagsFields" :fixed="true"> + </b-table> + </div> + </b-modal> + + <b-modal ref="series-preview" size="xl" ok-only="true"> + <template slot="modal-title"> + Preview of series + </template> + <div class="d-block text-center"> + <b-alert variant="danger" v-model="previewFailure"> + The remote DICOMweb server cannot generate a preview for this image. + </b-alert> + <b-img v-if="!previewFailure" :src="preview" fluid alt=""></b-img> + </div> + </b-modal> + + <b-modal id="series-delete-confirm" size="xl" @ok="ExecuteDeleteSeries"> + <template slot="modal-title"> + Confirm deletion + </template> + <div class="d-block"> + <p> + Are you sure you want to remove this series from the remote server? + </p> + <p> + Series description: {{ seriesToDelete && seriesToDelete['0008103E'] && seriesToDelete['0008103E'].Value }} + </p> + </div> + </b-modal> + </div> + + <p style="height:5em"></p> + </div> + + <!-- Add Vue and Bootstrap-Vue JS just before the closing </body> tag --> + <script src="../libs/js/vue.min.js"></script> + <script src="../libs/js/bootstrap-vue.min.js"></script> + <script src="../libs/js/axios.min.js"></script> + <script type="text/javascript" src="app.js"></script> + </body> +</html>