Added mammography plugin button on instance level
This commit is contained in:
commit
3e63ff2a37
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
WebApplication/node_modules
|
||||||
|
WebApplication/dist/
|
||||||
|
ThirdPartyDownloads/
|
||||||
|
.DS_Store
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
.vscode/
|
||||||
|
*~
|
||||||
|
build
|
||||||
27
AUTHORS
Normal file
27
AUTHORS
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
Orthanc Explorer 2
|
||||||
|
==================
|
||||||
|
|
||||||
|
|
||||||
|
Authors
|
||||||
|
-------
|
||||||
|
|
||||||
|
* Orthanc Team SRL <info@orthanc.team>
|
||||||
|
Rue Joseph Marchal 14
|
||||||
|
4910 Theux
|
||||||
|
Belgium
|
||||||
|
https://orthanc.team/
|
||||||
|
|
||||||
|
* Sebastien Jodogne <s.jodogne@orthanc-labs.com>
|
||||||
|
|
||||||
|
* Osimis S.A.
|
||||||
|
Quai Banning 6
|
||||||
|
4000 Liege
|
||||||
|
Belgium
|
||||||
|
|
||||||
|
* ICTEAM, UCLouvain
|
||||||
|
Place de l'Universite 1
|
||||||
|
1348 Ottignies-Louvain-la-Neuve
|
||||||
|
Belgium
|
||||||
|
https://uclouvain.be/icteam
|
||||||
|
|
||||||
|
* Luc Billaud <luc.billaud.pro@gmail.com>
|
||||||
14
CITATION.cff
Normal file
14
CITATION.cff
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
cff-version: "1.1.0"
|
||||||
|
message: "If you use this software, please cite it using these metadata."
|
||||||
|
title: Orthanc
|
||||||
|
abstract: "Orthanc is a lightweight open-source DICOM server for medical imaging supporting representational state transfer (REST)."
|
||||||
|
authors:
|
||||||
|
-
|
||||||
|
affiliation: UCLouvain
|
||||||
|
family-names: Jodogne
|
||||||
|
given-names: "Sébastien"
|
||||||
|
doi: "10.1007/s10278-018-0082-y"
|
||||||
|
license: "GPL-3.0-or-later"
|
||||||
|
repository-code: "https://orthanc.uclouvain.be/hg/orthanc/"
|
||||||
|
version: 1.12.3
|
||||||
|
date-released: 2024-01-31
|
||||||
237
CMakeLists.txt
Normal file
237
CMakeLists.txt
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2024 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, 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/>.
|
||||||
|
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.1)
|
||||||
|
|
||||||
|
if (APPLE)
|
||||||
|
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64" CACHE STRING "Build architectures for Mac OS X" FORCE)
|
||||||
|
set(CMAKE_CXX_STANDARD 11)
|
||||||
|
endif(APPLE)
|
||||||
|
|
||||||
|
project(OrthancExplorer2)
|
||||||
|
|
||||||
|
set(PLUGIN_VERSION "mainline" CACHE STRING "plugin version 'mainline' or '1.2.3'")
|
||||||
|
|
||||||
|
if (PLUGIN_VERSION MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
|
||||||
|
set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "1.12.2")
|
||||||
|
set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "web")
|
||||||
|
set(ORTHANC_OE2_VERSION ${PLUGIN_VERSION})
|
||||||
|
else()
|
||||||
|
set(ORTHANC_FRAMEWORK_DEFAULT_VERSION "mainline")
|
||||||
|
set(ORTHANC_FRAMEWORK_DEFAULT_SOURCE "hg")
|
||||||
|
set(ORTHANC_OE2_VERSION "mainline")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
# Parameters of the build
|
||||||
|
set(STATIC_BUILD OFF CACHE BOOL "Static build of the third-party libraries (necessary for Windows)")
|
||||||
|
set(STANDALONE_BUILD ON CACHE BOOL "Standalone build (all the resources are embedded, necessary for releases)")
|
||||||
|
set(ALLOW_DOWNLOADS ON CACHE BOOL "Allow CMake to download packages")
|
||||||
|
set(ORTHANC_FRAMEWORK_SOURCE "${ORTHANC_FRAMEWORK_DEFAULT_SOURCE}" CACHE STRING "Source of the Orthanc framework (can be \"system\", \"hg\", \"archive\", \"web\" or \"path\")")
|
||||||
|
set(ORTHANC_FRAMEWORK_VERSION "${ORTHANC_FRAMEWORK_DEFAULT_VERSION}" CACHE STRING "Version of the Orthanc framework")
|
||||||
|
set(ORTHANC_FRAMEWORK_ARCHIVE "" CACHE STRING "Path to the Orthanc archive, if ORTHANC_FRAMEWORK_SOURCE is \"archive\"")
|
||||||
|
set(ORTHANC_FRAMEWORK_ROOT "" CACHE STRING "Path to the Orthanc source directory, if ORTHANC_FRAMEWORK_SOURCE is \"path\"")
|
||||||
|
set(WEBAPP_DIST_PATH "${CMAKE_SOURCE_DIR}/WebApplication/dist" CACHE STRING "Path to the WebApplication/dist folder (the output of 'npm run build')")
|
||||||
|
set(WEBAPP_DIST_SOURCE "LOCAL" CACHE STRING "Source for the WebApplication/dist folder ('LOCAL' = assume it is build localy, 'WEB' = download it from web)")
|
||||||
|
set(WEBAPP_DIST_VERSION "${PLUGIN_VERSION}" CACHE STRING "Version of WebApplication/dist folder to download")
|
||||||
|
file(TO_CMAKE_PATH "${WEBAPP_DIST_PATH}" WEBAPP_DIST_PATH)
|
||||||
|
|
||||||
|
# Advanced parameters to fine-tune linking against system libraries
|
||||||
|
set(USE_SYSTEM_ORTHANC_SDK ON CACHE BOOL "Use the system version of the Orthanc plugin SDK")
|
||||||
|
set(ORTHANC_SDK_VERSION "1.11.2" CACHE STRING "Version of the Orthanc plugin SDK to use, if not using the system version (can be \"1.11.2\")")
|
||||||
|
set(ORTHANC_FRAMEWORK_STATIC OFF CACHE BOOL "If linking against the Orthanc framework system library, indicates whether this library was statically linked")
|
||||||
|
mark_as_advanced(ORTHANC_FRAMEWORK_STATIC)
|
||||||
|
|
||||||
|
|
||||||
|
# Download and setup the Orthanc framework
|
||||||
|
include(${CMAKE_SOURCE_DIR}/Resources/Orthanc/CMake/DownloadOrthancFramework.cmake)
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "system")
|
||||||
|
if (ORTHANC_FRAMEWORK_USE_SHARED)
|
||||||
|
include(FindBoost)
|
||||||
|
find_package(Boost COMPONENTS regex thread)
|
||||||
|
|
||||||
|
if (NOT Boost_FOUND)
|
||||||
|
message(FATAL_ERROR "Unable to locate Boost on this system")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
link_libraries(${Boost_LIBRARIES} jsoncpp)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
link_libraries(${ORTHANC_FRAMEWORK_LIBRARIES})
|
||||||
|
|
||||||
|
set(USE_SYSTEM_GOOGLE_TEST ON CACHE BOOL "Use the system version of Google Test")
|
||||||
|
set(USE_GOOGLE_TEST_DEBIAN_PACKAGE OFF CACHE BOOL "Use the sources of Google Test shipped with libgtest-dev (Debian only)")
|
||||||
|
mark_as_advanced(USE_GOOGLE_TEST_DEBIAN_PACKAGE)
|
||||||
|
include(${CMAKE_SOURCE_DIR}/Resources/Orthanc/CMake/GoogleTestConfiguration.cmake)
|
||||||
|
|
||||||
|
else()
|
||||||
|
include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkParameters.cmake)
|
||||||
|
|
||||||
|
set(ENABLE_LOCALE OFF) # Enable support for locales (notably in Boost)
|
||||||
|
#set(ENABLE_GOOGLE_TEST ON)
|
||||||
|
#set(ENABLE_WEB_CLIENT ON)
|
||||||
|
|
||||||
|
# Those modules of the Orthanc framework are not needed
|
||||||
|
set(ENABLE_MODULE_IMAGES OFF)
|
||||||
|
set(ENABLE_MODULE_JOBS OFF)
|
||||||
|
set(ENABLE_MODULE_DICOM OFF)
|
||||||
|
|
||||||
|
include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkConfiguration.cmake)
|
||||||
|
include_directories(${ORTHANC_FRAMEWORK_ROOT})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
include(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Plugins/OrthancPluginsExports.cmake)
|
||||||
|
|
||||||
|
|
||||||
|
if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK)
|
||||||
|
if (ORTHANC_SDK_VERSION STREQUAL "1.11.2")
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/Resources/Orthanc/Sdk-1.11.2)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "Unsupported version of the Orthanc plugin SDK: ${ORTHANC_SDK_VERSION}")
|
||||||
|
endif()
|
||||||
|
else ()
|
||||||
|
CHECK_INCLUDE_FILE_CXX(orthanc/OrthancCPlugin.h HAVE_ORTHANC_H)
|
||||||
|
if (NOT HAVE_ORTHANC_H)
|
||||||
|
message(FATAL_ERROR "Please install the headers of the Orthanc plugins SDK")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
|
||||||
|
${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
|
||||||
|
${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
|
||||||
|
link_libraries(rt)
|
||||||
|
elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
|
||||||
|
SET(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -lws2_32")
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND
|
||||||
|
${PYTHON_EXECUTABLE} ${ORTHANC_FRAMEWORK_ROOT}/../Resources/WindowsResources.py
|
||||||
|
${ORTHANC_OE2_VERSION} "Orthanc Explorer 2 plugin" OrthancExplorer2.dll
|
||||||
|
"Advanced User Interface Plugin for Orthanc"
|
||||||
|
ERROR_VARIABLE Failure
|
||||||
|
OUTPUT_FILE ${CMAKE_CURRENT_BINARY_DIR}/Version.rc
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Failure)
|
||||||
|
message(FATAL_ERROR "Error while computing the version information: ${Failure}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
list(APPEND AUTOGENERATED_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/Version.rc)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
if (APPLE)
|
||||||
|
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework CoreFoundation")
|
||||||
|
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -framework CoreFoundation")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (${WEBAPP_DIST_SOURCE} STREQUAL "WEB")
|
||||||
|
SET(WEBAPP_DIST_PATH "${CMAKE_BINARY_DIR}/dist") # downloading the WEB version -> don't modify source folder
|
||||||
|
if (${WEBAPP_DIST_VERSION} STREQUAL "mainline")
|
||||||
|
set(WEBAPP_DIST_VERSION "master") # the branch is named master, not mainline
|
||||||
|
endif()
|
||||||
|
DownloadPackage(no-check "https://public-files.orthanc.team/oe2-dist-files/${WEBAPP_DIST_VERSION}.zip" ${WEBAPP_DIST_PATH})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (STANDALONE_BUILD)
|
||||||
|
add_definitions(-DORTHANC_STANDALONE=1)
|
||||||
|
set(ADDITIONAL_RESOURCES
|
||||||
|
DEFAULT_CONFIGURATION ${CMAKE_SOURCE_DIR}/Plugin/DefaultConfiguration.json
|
||||||
|
WEB_APPLICATION_ASSETS ${WEBAPP_DIST_PATH}/assets
|
||||||
|
WEB_APPLICATION_FAVICON ${WEBAPP_DIST_PATH}/favicon.ico
|
||||||
|
WEB_APPLICATION_INDEX ${WEBAPP_DIST_PATH}/index.html
|
||||||
|
WEB_APPLICATION_INDEX_LANDING ${WEBAPP_DIST_PATH}/token-landing.html
|
||||||
|
WEB_APPLICATION_INDEX_RETRIEVE_AND_VIEW ${WEBAPP_DIST_PATH}/retrieve-and-view.html
|
||||||
|
DEFAULT_CSS_LIGHT ${CMAKE_SOURCE_DIR}/WebApplication/src/assets/css/defaults-light.css
|
||||||
|
DEFAULT_CSS_DARK ${CMAKE_SOURCE_DIR}/WebApplication/src/assets/css/defaults-dark.css
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
add_definitions(-DORTHANC_STANDALONE=0)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
EmbedResources(
|
||||||
|
--no-upcase-check
|
||||||
|
${ADDITIONAL_RESOURCES}
|
||||||
|
ORTHANC_EXPLORER ${CMAKE_SOURCE_DIR}/Plugin/OrthancExplorer.js
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# As the embedded resources are shared by both the "UnitTests" and the
|
||||||
|
# "OrthancExplorer2" targets, avoid race conditions in the code
|
||||||
|
# generation by adding a target between them
|
||||||
|
add_custom_target(
|
||||||
|
AutogeneratedTarget
|
||||||
|
DEPENDS
|
||||||
|
${AUTOGENERATED_SOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
add_definitions(
|
||||||
|
-DHAS_ORTHANC_EXCEPTION=1
|
||||||
|
-DORTHANC_ENABLE_LOGGING_PLUGIN=1
|
||||||
|
)
|
||||||
|
|
||||||
|
set(CORE_SOURCES
|
||||||
|
# Plugin/Configuration.cpp
|
||||||
|
|
||||||
|
${CMAKE_SOURCE_DIR}/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
|
||||||
|
${ORTHANC_CORE_SOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(OrthancExplorer2 SHARED ${CORE_SOURCES}
|
||||||
|
${CMAKE_SOURCE_DIR}/Plugin/Plugin.cpp
|
||||||
|
${AUTOGENERATED_SOURCES}
|
||||||
|
)
|
||||||
|
|
||||||
|
add_dependencies(OrthancExplorer2 AutogeneratedTarget)
|
||||||
|
|
||||||
|
DefineSourceBasenameForTarget(OrthancExplorer2)
|
||||||
|
|
||||||
|
message("Setting the version of the library to ${ORTHANC_OE2_VERSION}")
|
||||||
|
|
||||||
|
add_definitions(-DORTHANC_OE2_VERSION="${ORTHANC_OE2_VERSION}")
|
||||||
|
|
||||||
|
set_target_properties(OrthancExplorer2 PROPERTIES
|
||||||
|
VERSION ${ORTHANC_OE2_VERSION}
|
||||||
|
SOVERSION ${ORTHANC_OE2_VERSION}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(
|
||||||
|
TARGETS OrthancExplorer2
|
||||||
|
RUNTIME DESTINATION lib # Destination for Windows
|
||||||
|
LIBRARY DESTINATION share/orthanc/plugins # Destination for Linux
|
||||||
|
)
|
||||||
|
|
||||||
|
# add_executable(UnitTests
|
||||||
|
# ${AUTOGENERATED_SOURCES}
|
||||||
|
# ${CORE_SOURCES}
|
||||||
|
# ${GOOGLE_TEST_SOURCES}
|
||||||
|
# # UnitTestsSources/UnitTestsMain.cpp
|
||||||
|
# )
|
||||||
|
|
||||||
|
# add_dependencies(UnitTests AutogeneratedTarget)
|
||||||
|
|
||||||
|
# target_link_libraries(UnitTests
|
||||||
|
# ${GOOGLE_TEST_LIBRARIES}
|
||||||
|
# )
|
||||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
297
Plugin/DefaultConfiguration.json
Normal file
297
Plugin/DefaultConfiguration.json
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
{
|
||||||
|
"OrthancExplorer2" : {
|
||||||
|
// enables or disables the plugin completely
|
||||||
|
"Enable": true,
|
||||||
|
|
||||||
|
// Prefix URL of the OE2 application (and API)
|
||||||
|
// A value of '/my-ui' means that the app will be available under /my-ui/app/
|
||||||
|
// and the api will be available under /my-ui/api/...
|
||||||
|
// Should start and end with a '/'
|
||||||
|
"Root" : "/ui/",
|
||||||
|
|
||||||
|
// Whether OE2 shall replace the default Orthanc Explorer interface ('/' URL will redirect to OE2)
|
||||||
|
"IsDefaultOrthancUI": true,
|
||||||
|
|
||||||
|
// Base theme for the UI (before custom CSS are applied).
|
||||||
|
// Allowed values: "light", "dark"
|
||||||
|
"Theme": "light",
|
||||||
|
|
||||||
|
// Path to custom CSS file or logo.
|
||||||
|
// The custom CSS are applied after these default files and they are loaded after all other CSS.
|
||||||
|
// You may actually override any CSS value or variable from the application.
|
||||||
|
// - https://github.com/orthanc-server/orthanc-explorer-2/blob/master/WebApplication/src/assets/css/defaults-dark.css
|
||||||
|
// - https://github.com/orthanc-server/orthanc-explorer-2/blob/master/WebApplication/src/assets/css/defaults-light.css
|
||||||
|
// "CustomCssPath": "/home/my/path/to/custom.css",
|
||||||
|
// "CustomLogoUrl": "https://my.company/logo.png",
|
||||||
|
// "CustomLogoPath": "/home/my/path/to/logo.png",
|
||||||
|
// "CustomFavIconPath": "/home/my/path/to/favicon.ico", // note: this can be a .png, .svg or .ico file
|
||||||
|
|
||||||
|
// Custom Window/Tab Title
|
||||||
|
// "CustomTitle": "Orthanc Explorer 2",
|
||||||
|
|
||||||
|
// This block of configuration is transmitted as is to the frontend application.
|
||||||
|
// Make sure not to store any secret here
|
||||||
|
"UiOptions" : {
|
||||||
|
// note: all the "Enable..." variables can be set to false by the user-profile (if using the authorization plugin)
|
||||||
|
|
||||||
|
"EnableStudyList": true, // Enables the access to the study list (TODO)
|
||||||
|
"EnableUpload": true, // Enables the upload menu/interface
|
||||||
|
"EnableDicomModalities": true, // Enables the 'DICOM Modalities' interface in the side menu
|
||||||
|
"EnableDeleteResources": true, // Enables the delete button for Studies/Series/Instances
|
||||||
|
"EnableDownloadZip": true, // Enables the download zip button for Studies/Series
|
||||||
|
"EnableDownloadDicomDir": false, // Enables the download DICOM DIR button for Studies/Series
|
||||||
|
"EnableDownloadDicomFile": true, // Enables the download DICOM file button for Instances
|
||||||
|
"EnableAnonymization": true, // Enables the anonymize button
|
||||||
|
"EnableModification": true, // Enables the modify button
|
||||||
|
"EnableSendTo": true, // Enables the 'SendTo' button for Studies/Series/Instances
|
||||||
|
"EnableApiViewMenu": false, // Enables the API button to open API routes for Studies/Series/Instances (developer mode)
|
||||||
|
"EnableSettings": true, // Enables the settings menu/interface
|
||||||
|
"EnableLinkToLegacyUi": true, // Enables a link to the legacy Orthanc UI
|
||||||
|
"EnableChangePassword": true, // Enables the 'change password' button in the side bar. Only applicable if Keycloak is enabled
|
||||||
|
"EnableViewerQuickButton": true, // Enables a button in the study list to directly open a viewer
|
||||||
|
"EnableReportQuickButton": false, // Enables a button in the study list to directly open a PDF report if available in the study
|
||||||
|
|
||||||
|
"EnableEditLabels": true, // Enables labels management (create/delete/assign/unassign)
|
||||||
|
"AvailableLabels": [], // If not empty, this list prevents the creation of new labels and only allows add/remove of the listed labels.
|
||||||
|
// This configuration may be overriden when you use Keycloak and an auth-service that implements roles/permissions API.
|
||||||
|
"EnableLabelsCount": true, // Enables display of study count next to the each label (this might slow down the UI)
|
||||||
|
|
||||||
|
"EnableShares": false, // Enables sharing studies. See "Tokens" section below.
|
||||||
|
"DefaultShareDuration": 0, // [in days]. 0 means no expiration date,
|
||||||
|
"ShareDurations": [0, 7, 15, 30, 90, 365], // The share durations proposed in the UI
|
||||||
|
|
||||||
|
"EnableAddSeries": true, // Enables the "Add series" button
|
||||||
|
"AddSeriesDefaultTags": { // Default tag values when adding a new series of each type ("pdf", "image" or "stl")
|
||||||
|
"pdf" : {
|
||||||
|
"SeriesDescription": "Report",
|
||||||
|
"Modality": "DOC",
|
||||||
|
"SeriesDate": "$TODAY$" // Allowed keywords: $TODAY$, $STUDY_DATE$, $FILE_DATE$
|
||||||
|
},
|
||||||
|
"image" : {
|
||||||
|
"SeriesDescription": "Picture",
|
||||||
|
"Modality": "XC",
|
||||||
|
"SeriesDate": "$TODAY$"
|
||||||
|
},
|
||||||
|
"stl" : {
|
||||||
|
"SeriesDescription": "Model",
|
||||||
|
"Modality": "M3D",
|
||||||
|
"SeriesDate": "$TODAY$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"EnabledMammographyPlugin": false,
|
||||||
|
|
||||||
|
// If both OHIF viewer configurations are enabled, only the v3 is taken into account
|
||||||
|
"EnableOpenInOhifViewer": false, // Enables a "open in OHIF viewer" button. Note: OHIF can not be used together with KeyCloak (https://community.ohif.org/t/ohif-orthanc-token-to-access-a-single-study/727)
|
||||||
|
"OhifViewerPublicRoot" : "http://to-be-defined/", // must end with a '/'. e.g: "http://ohif.my.site/" means that link to OHIF will look like http://ohif.my.site/Viewer/1.2.3.444.5555....
|
||||||
|
"EnableOpenInOhifViewer3": false, // Enables a "open in OHIF viewer" button. If the OHIF plugin is loaded, the default value is 'true'
|
||||||
|
"OhifViewer3PublicRoot" : "/ohif/", // must end with a '/'. e.g: "http://ohif.my.site/" means that link to OHIF will look like http://ohif.my.site/viewer?StudyInstanceUIDs=1.2.3.444.5555....
|
||||||
|
|
||||||
|
"EnableOpenInMedDreamViewer": false, // Enables a "open in MedDream viewer" button
|
||||||
|
"MedDreamViewerPublicRoot" : "http://to-be-defined/", // must end with a '/'. e.g: "http://meddream.my.site/Viewer/" means that link to MedDream will look like http://meddream.my.site/?study=1.2.3.444.5555....
|
||||||
|
|
||||||
|
// Defines which icons is used by which enabled viewer.
|
||||||
|
// Allowed keys: "meddream", "osimis-web-viewer", "stone-webviewer", "ohif", "ohif-vr", "ohif-tmtv"
|
||||||
|
// Allowed values: CSS class that defines the viewer icons (only from bootstrap icons)
|
||||||
|
"ViewersIcons" : {
|
||||||
|
"osimis-web-viewer": "bi bi-eye",
|
||||||
|
"stone-webviewer": "bi bi-eye-fill",
|
||||||
|
"ohif": "bi bi-grid",
|
||||||
|
"ohif-vr": "bi bi-grid-1x2",
|
||||||
|
"ohif-tmtv": "bi bi-grid-3x3-gap",
|
||||||
|
"ohif-seg": "fa-solid fa-shapes fa-button",
|
||||||
|
"ohif-micro": "fa-solid fa-microscope fa-button",
|
||||||
|
"meddream": "bi bi-columns-gap",
|
||||||
|
"volview": "bi bi-box",
|
||||||
|
"wsi": "fa-solid fa-microscope fa-button"
|
||||||
|
},
|
||||||
|
// Defines the order in which the viewer icons should appear in the interface
|
||||||
|
// OHIF viewers modes that are not listed here, won't appear in the interface.
|
||||||
|
"ViewersOrdering" : [
|
||||||
|
// "osimis-web-viewer", // now deprecated
|
||||||
|
"stone-webviewer",
|
||||||
|
"ohif",
|
||||||
|
"ohif-vr",
|
||||||
|
"ohif-tmtv",
|
||||||
|
"ohif-seg",
|
||||||
|
// "ohif-micro", // currently disabled, this is still experimental in OHIF
|
||||||
|
"meddream",
|
||||||
|
"volview",
|
||||||
|
"wsi"
|
||||||
|
],
|
||||||
|
|
||||||
|
"MaxStudiesDisplayed": 100, // The maximum number of studies displayed in the study list.
|
||||||
|
// From v 1.7.0, this option is not used anymore in the local study list when using
|
||||||
|
// a DB backend that supports ExtendedFind (SQLite and PostgreSQL)
|
||||||
|
// but is still used in the DicomWeb queries.
|
||||||
|
"PageLoadSize": 50, // The number of items that are loaded when scrolling the study or instance list.
|
||||||
|
// Only applicable with a DB backend that supports ExtendedFind.
|
||||||
|
|
||||||
|
"MaxMyJobsHistorySize": 5, // The maximum number of jobs appearing under 'my jobs' in side bar (0 = unlimited)
|
||||||
|
|
||||||
|
"StudyListSearchMode": "search-as-you-type",// mode to trigger a search in the StudyList. Accepted values: 'search-as-you-type' or 'search-button'
|
||||||
|
"StudyListSearchAsYouTypeMinChars": 3, // minimum number of characters to enter in a text search field before it starts searching the DB
|
||||||
|
"StudyListSearchAsYouTypeDelay": 400, // Delay [ms] between the last key stroke and the trigger of the search
|
||||||
|
"StudyListContentIfNoSearch": "most-recents", // Defines what to show if no search criteria has been entered
|
||||||
|
// Allowed values: "empty", "most-recents"
|
||||||
|
// From v 1.7.0, this option is always considered as "most-recents" when using
|
||||||
|
// a DB backend that supports ExtendedFind (SQLite and PostgreSQL)
|
||||||
|
|
||||||
|
// Default settings are ok for "small" Orthanc Databases. For large databases, it is recommended to use these settings:
|
||||||
|
// "StudyListSearchMode": "search-button"
|
||||||
|
// "StudyListContentIfNoSearch": "empty"
|
||||||
|
|
||||||
|
"ShowOrthancName": true, // display the Orthanc Name in the side menu
|
||||||
|
|
||||||
|
// The list of tags to be displayed in the upload dialog result list
|
||||||
|
// (the first N defined tags in the list are displayed on the UI)
|
||||||
|
// Allowed values are: "StudyDate", "AccessionNumber", "PatientID",
|
||||||
|
// "PatientName", "PatientBirthDate", "StudyDescription"
|
||||||
|
"UploadReportTags" : [
|
||||||
|
"PatientName",
|
||||||
|
"StudyDescription",
|
||||||
|
"PatientID",
|
||||||
|
"AccessionNumber",
|
||||||
|
"StudyDate"
|
||||||
|
],
|
||||||
|
"UploadReportMaxTags" : 2, // See above, the max number of tags displayed in the upload report
|
||||||
|
|
||||||
|
// The ordered list of columns to display in the study list.
|
||||||
|
// Allowed values are:
|
||||||
|
// - Dicom Tags: "StudyDate", "AccessionNumber", "PatientID"
|
||||||
|
// "PatientName", "PatientBirthDate", "StudyDescription"
|
||||||
|
// - special columns:
|
||||||
|
// - "modalities": the list of modalities in the study
|
||||||
|
// - "seriesCount": the number of series in the study
|
||||||
|
// - "instancesCount": the number of instances in the study
|
||||||
|
// - "seriesAndInstancesCount": a combined value with the number of series/instances in the study
|
||||||
|
"StudyListColumns" : [
|
||||||
|
"PatientBirthDate",
|
||||||
|
"PatientName",
|
||||||
|
"PatientID",
|
||||||
|
"StudyDescription",
|
||||||
|
"StudyDate",
|
||||||
|
"modalities",
|
||||||
|
"AccessionNumber",
|
||||||
|
"seriesAndInstancesCount"
|
||||||
|
],
|
||||||
|
|
||||||
|
// The list of patient level tags that are displayed in the study details and in the modification dialog.
|
||||||
|
// Note that these tags must be defined in the Orthanc main dicom tags (https://orthanc.uclouvain.be/book/faq/main-dicom-tags.html)
|
||||||
|
"PatientMainTags" : [
|
||||||
|
"PatientID",
|
||||||
|
"PatientName",
|
||||||
|
"PatientBirthDate",
|
||||||
|
"PatientSex",
|
||||||
|
"OtherPatientIDs"
|
||||||
|
],
|
||||||
|
|
||||||
|
// The list of study level tags that are displayed in the study details and in the modification dialog.
|
||||||
|
// Note that these tags must be defined in the Orthanc main dicom tags (https://orthanc.uclouvain.be/book/faq/main-dicom-tags.html)
|
||||||
|
"StudyMainTags" : [
|
||||||
|
"StudyDate",
|
||||||
|
"StudyTime",
|
||||||
|
"StudyDescription",
|
||||||
|
"AccessionNumber",
|
||||||
|
"StudyID",
|
||||||
|
"StudyInstanceUID",
|
||||||
|
"RequestingPhysician",
|
||||||
|
"ReferringPhysicianName",
|
||||||
|
"InstitutionName"
|
||||||
|
],
|
||||||
|
|
||||||
|
// The list of series level tags that are displayed in the study details and in the modification dialog.
|
||||||
|
// Note that these tags must be defined in the Orthanc main dicom tags (https://orthanc.uclouvain.be/book/faq/main-dicom-tags.html)
|
||||||
|
"SeriesMainTags" : [
|
||||||
|
"SeriesDate",
|
||||||
|
"SeriesTime",
|
||||||
|
"SeriesDescription",
|
||||||
|
"SeriesNumber",
|
||||||
|
"BodyPartExamined",
|
||||||
|
"ProtocolName",
|
||||||
|
"SeriesInstanceUID"
|
||||||
|
],
|
||||||
|
|
||||||
|
// The modalities to display in the Modalities filter dropdown in the Study List
|
||||||
|
"ModalitiesFilter": [
|
||||||
|
"CR", "CT", "DOC", "DR", "DX", "KO", "MG", "MR", "NM", "OT", "PR", "PT", "PX", "RTDOSE", "RTSTRUCT", "RTPLAN", "SEG", "SR", "US", "XA", "XC"
|
||||||
|
],
|
||||||
|
|
||||||
|
// Defines the list of languages available in the language picker
|
||||||
|
// ex: "AvailableLanguages" : ["en", "fr"],
|
||||||
|
// ex: "AvailableLanguages" : [] -> this won't show the language picker at all and force usage of the DefaultLanguage
|
||||||
|
"AvailableLanguages": ["en", "de", "es", "fr", "it", "ka", "ru", "si", "uk", "zh"],
|
||||||
|
|
||||||
|
// Force the default language. If null (default), the language is the language from the visitor's browser.
|
||||||
|
// ex: "DefaultLanguage" : "en"
|
||||||
|
"DefaultLanguage" : null,
|
||||||
|
|
||||||
|
// Should DicomTags be translated (true) or shall we use the English symbolic name whatever the selected language (false)
|
||||||
|
// if true: "PatientID" is displayed as e.g "ID Patient" in french
|
||||||
|
// if false: "PatientID" is displayed as "PatientID" in all languages
|
||||||
|
"TranslateDicomTags" : true,
|
||||||
|
|
||||||
|
// Display format for dates in the study list based on these definitions: https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
|
||||||
|
// example: "dd/MM/yyyy" for European Format,
|
||||||
|
// "yyyyMMdd" for DICOM format
|
||||||
|
"DateFormat": "yyyyMMdd",
|
||||||
|
|
||||||
|
// PatientName transformation for display. This requires a Regex for capturing and one expression for formatting.
|
||||||
|
// If PatientNameFormatting is not defined, no transformation occurs (this is the default behaviour).
|
||||||
|
// The Regex is a Javascript regex and is applied this way:
|
||||||
|
// FormattedName = PatientName.replace(new RegExp(PatientNameCapture), PatientNameFormatting);
|
||||||
|
// in example, to capture carets separated names and re-order them, one would use:
|
||||||
|
// FormattedName = PatientName.replace(new RegExp("([^\\^]+)\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?"), '$1 $3 $2');
|
||||||
|
"PatientNameCapture" : "([^\\^]+)\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?",
|
||||||
|
// "PatientNameFormatting": "$1 $3 $2"
|
||||||
|
|
||||||
|
// modifications dialog options
|
||||||
|
"Modifications": {
|
||||||
|
// Modes define options to the /modify route:
|
||||||
|
// "modify-keep-uids" is equivalent to KeepSource=True and Keep=["StudyInstanceUID", "SeriesInstanceUID", "SOPInstanceUID"]
|
||||||
|
// "modify-new-uids" is equivalent to KeepSource=False
|
||||||
|
// "duplicate" is equivalent to KeepSource=True
|
||||||
|
// "AllowedModes" and "DefaultMode" apply to studies modification
|
||||||
|
"AllowedModes": ["modify-new-uids", "modify-keep-uids", "duplicate"],
|
||||||
|
"DefaultMode": "modify-new-uids",
|
||||||
|
|
||||||
|
// same configurations for Series (introduced in 1.2.0)
|
||||||
|
"SeriesAllowedModes": ["modify-new-uids", "modify-keep-uids", "duplicate"],
|
||||||
|
"SeriesDefaultMode": "modify-new-uids"
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
// List of tags that are used to identify studies belonging to the same patient.
|
||||||
|
// This appears as "This patient has {count} studies in total" in the study details.
|
||||||
|
// ex: "ShowSamePatientStudiesFilter" : ["PatientID", "PatientBirthDate", "PatientSex"]
|
||||||
|
"ShowSamePatientStudiesFilter" : [
|
||||||
|
"PatientID"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"Shares" : {
|
||||||
|
"TokenService" : {
|
||||||
|
"Url": "http://change-me:8000/shares",
|
||||||
|
"Username": "change-me",
|
||||||
|
"Password": "change-me"
|
||||||
|
},
|
||||||
|
"Type": "osimis-viewer-publication" // allowed values: "osimis-viewer-publication", "meddream-viewer-publication", "stone-viewer-publication"
|
||||||
|
},
|
||||||
|
|
||||||
|
// When using Keycloak for user management
|
||||||
|
"Keycloak" : {
|
||||||
|
"Enable": false,
|
||||||
|
"Url": "http://change-me:8080/",
|
||||||
|
"Realm": "change-me",
|
||||||
|
"ClientId": "change-me"
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// this section is only relevant if the authorization plugin is enabled and user-profile based permissions are implemented
|
||||||
|
"Tokens" : {
|
||||||
|
"InstantLinksValidity": 200, // the duration, in seconds an 'instant' token is valid (e.g. used to download a study, open a study in a viewer)
|
||||||
|
"ShareType": "stone-viewer-publication" // allowed values: "stone-viewer-publication", "osimis-viewer-publication", "meddream-viewer-publication"
|
||||||
|
//"RequiredForLinks": false // experimental, set it to false when using basic-auth together with the auth-plugin (https://discourse.orthanc-server.org/t/user-based-access-control-with-label-based-resource-access/5454)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
Plugin/OrthancExplorer.js
Normal file
23
Plugin/OrthancExplorer.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
$('#lookup').live('pagebeforeshow', function() {
|
||||||
|
$('#open-oe2').remove();
|
||||||
|
|
||||||
|
var b = $('<fieldset>')
|
||||||
|
.attr('id', 'open-oe2')
|
||||||
|
.addClass('ui-grid-b')
|
||||||
|
.append($('<div>')
|
||||||
|
.addClass('ui-block-a'))
|
||||||
|
.append($('<div>')
|
||||||
|
.addClass('ui-block-b')
|
||||||
|
.append($('<a>')
|
||||||
|
.attr('data-role', 'button')
|
||||||
|
.attr('href', '#')
|
||||||
|
.attr('data-icon', 'forward')
|
||||||
|
.attr('data-theme', 'a')
|
||||||
|
.text('Open Orthanc Explorer 2')
|
||||||
|
.button()
|
||||||
|
.click(function(e) {
|
||||||
|
window.open('../${OE2_BASE_URL}/app/index.html');
|
||||||
|
})));
|
||||||
|
|
||||||
|
b.insertAfter($('#lookup-result'));
|
||||||
|
});
|
||||||
933
Plugin/Plugin.cpp
Normal file
933
Plugin/Plugin.cpp
Normal file
@ -0,0 +1,933 @@
|
|||||||
|
/**
|
||||||
|
* Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
* Department, University Hospital of Liege, Belgium
|
||||||
|
* Copyright (C) 2017-2024 Osimis S.A., Belgium
|
||||||
|
* Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
* Copyright (C) 2024-2024 Orthanc Team SRL, 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/>.
|
||||||
|
**/
|
||||||
|
|
||||||
|
#include "../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
|
||||||
|
|
||||||
|
#include <Logging.h>
|
||||||
|
#include <SystemToolbox.h>
|
||||||
|
#include <Toolbox.h>
|
||||||
|
#include <SerializationToolbox.h>
|
||||||
|
|
||||||
|
#include <EmbeddedResources.h>
|
||||||
|
|
||||||
|
#define ORTHANC_PLUGIN_NAME "orthanc-explorer-2"
|
||||||
|
|
||||||
|
// we are using Orthanc 1.11.0 API (RequestedTags in tools/find)
|
||||||
|
#define ORTHANC_CORE_MINIMAL_MAJOR 1
|
||||||
|
#define ORTHANC_CORE_MINIMAL_MINOR 11
|
||||||
|
#define ORTHANC_CORE_MINIMAL_REVISION 0
|
||||||
|
|
||||||
|
|
||||||
|
std::unique_ptr<OrthancPlugins::OrthancConfiguration> orthancFullConfiguration_;
|
||||||
|
Json::Value pluginJsonConfiguration_;
|
||||||
|
std::string oe2BaseUrl_;
|
||||||
|
|
||||||
|
Json::Value pluginsConfiguration_;
|
||||||
|
bool hasUserProfile_ = false;
|
||||||
|
bool openInOhifV3IsExplicitelyDisabled = false;
|
||||||
|
bool enableShares_ = false;
|
||||||
|
bool isReadOnly_ = false;
|
||||||
|
std::string customCssPath_;
|
||||||
|
std::string theme_ = "light";
|
||||||
|
std::string customLogoPath_;
|
||||||
|
std::string customLogoUrl_;
|
||||||
|
std::string customFavIconPath_;
|
||||||
|
std::string customTitle_;
|
||||||
|
|
||||||
|
enum CustomFilesPath
|
||||||
|
{
|
||||||
|
CustomFilesPath_Logo,
|
||||||
|
CustomFilesPath_FavIcon
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
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]);
|
||||||
|
Orthanc::MimeType mimeType = Orthanc::SystemToolbox::AutodetectMimeType(path);
|
||||||
|
const char* mime = Orthanc::EnumerationToString(mimeType);
|
||||||
|
|
||||||
|
std::string fileContent;
|
||||||
|
Orthanc::EmbeddedResources::GetDirectoryResource(fileContent, folder, path.c_str());
|
||||||
|
|
||||||
|
const char* resource = fileContent.size() ? fileContent.c_str() : NULL;
|
||||||
|
OrthancPluginAnswerBuffer(context, output, resource, fileContent.size(), mime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <enum Orthanc::EmbeddedResources::FileResourceId file, Orthanc::MimeType mime>
|
||||||
|
void ServeEmbeddedFile(OrthancPluginRestOutput* output,
|
||||||
|
const char* url,
|
||||||
|
const OrthancPluginHttpRequest* request)
|
||||||
|
{
|
||||||
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
||||||
|
|
||||||
|
if (request->method != OrthancPluginHttpMethod_Get)
|
||||||
|
{
|
||||||
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::string s;
|
||||||
|
Orthanc::EmbeddedResources::GetFileResource(s, file);
|
||||||
|
|
||||||
|
if (file == Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX && theme_ != "light")
|
||||||
|
{
|
||||||
|
boost::replace_all(s, "data-bs-theme=\"light\"", "data-bs-theme=\"" + theme_ + "\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* resource = s.size() ? s.c_str() : NULL;
|
||||||
|
OrthancPluginAnswerBuffer(context, output, resource, s.size(), Orthanc::EnumerationToString(mime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <enum CustomFilesPath customFile>
|
||||||
|
void ServeCustomFile(OrthancPluginRestOutput* output,
|
||||||
|
const char* url,
|
||||||
|
const OrthancPluginHttpRequest* request)
|
||||||
|
{
|
||||||
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
||||||
|
|
||||||
|
if (request->method != OrthancPluginHttpMethod_Get)
|
||||||
|
{
|
||||||
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::string fileContent;
|
||||||
|
std::string customFileContent;
|
||||||
|
std::string customFilePath;
|
||||||
|
if (customFile == CustomFilesPath_FavIcon)
|
||||||
|
{
|
||||||
|
customFilePath = customFavIconPath_;
|
||||||
|
}
|
||||||
|
else if (customFile == CustomFilesPath_Logo)
|
||||||
|
{
|
||||||
|
customFilePath = customLogoPath_;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
|
||||||
|
}
|
||||||
|
|
||||||
|
Orthanc::SystemToolbox::ReadFile(fileContent, customFilePath);
|
||||||
|
Orthanc::MimeType mimeType = Orthanc::SystemToolbox::AutodetectMimeType(customFilePath);
|
||||||
|
|
||||||
|
// include an ETag for correct cache handling
|
||||||
|
OrthancPlugins::OrthancString md5;
|
||||||
|
size_t size = fileContent.size();
|
||||||
|
md5.Assign(OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), fileContent.c_str(), size));
|
||||||
|
|
||||||
|
std::string etag = "\"" + std::string(md5.GetContent()) + "\"";
|
||||||
|
OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "ETag", etag.c_str());
|
||||||
|
OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Cache-Control", "no-cache");
|
||||||
|
|
||||||
|
OrthancPluginAnswerBuffer(context, output, fileContent.c_str(), size, Orthanc::EnumerationToString(mimeType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// serves either the default CSS or a custom file CSS
|
||||||
|
void ServeCustomCss(OrthancPluginRestOutput* output,
|
||||||
|
const char* url,
|
||||||
|
const OrthancPluginHttpRequest* request)
|
||||||
|
{
|
||||||
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
||||||
|
|
||||||
|
if (request->method != OrthancPluginHttpMethod_Get)
|
||||||
|
{
|
||||||
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::string cssFileContent;
|
||||||
|
|
||||||
|
if (strstr(url, "custom.css") != NULL)
|
||||||
|
{
|
||||||
|
if (theme_ == "dark")
|
||||||
|
{
|
||||||
|
Orthanc::EmbeddedResources::GetFileResource(cssFileContent, Orthanc::EmbeddedResources::DEFAULT_CSS_DARK);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Orthanc::EmbeddedResources::GetFileResource(cssFileContent, Orthanc::EmbeddedResources::DEFAULT_CSS_LIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customCssPath_.empty())
|
||||||
|
{ // append the custom CSS
|
||||||
|
std::string customCssFileContent;
|
||||||
|
Orthanc::SystemToolbox::ReadFile(customCssFileContent, customCssPath_);
|
||||||
|
cssFileContent += "\n/* Appending the custom CSS */\n" + customCssFileContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* resource = cssFileContent.size() ? cssFileContent.c_str() : NULL;
|
||||||
|
size_t size = cssFileContent.size();
|
||||||
|
|
||||||
|
// include an ETag for correct cache handling
|
||||||
|
OrthancPlugins::OrthancString md5;
|
||||||
|
md5.Assign(OrthancPluginComputeMd5(OrthancPlugins::GetGlobalContext(), resource, size));
|
||||||
|
|
||||||
|
std::string etag = "\"" + std::string(md5.GetContent()) + "\"";
|
||||||
|
OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "ETag", etag.c_str());
|
||||||
|
OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Cache-Control", "no-cache");
|
||||||
|
|
||||||
|
OrthancPluginAnswerBuffer(context, output, resource, size, Orthanc::EnumerationToString(Orthanc::MimeType_Css));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void RedirectRoot(OrthancPluginRestOutput* output,
|
||||||
|
const char* url,
|
||||||
|
const OrthancPluginHttpRequest* request)
|
||||||
|
{
|
||||||
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
||||||
|
|
||||||
|
if (request->method != OrthancPluginHttpMethod_Get)
|
||||||
|
{
|
||||||
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (uint32_t i = 0; i < request->headersCount; ++i)
|
||||||
|
{
|
||||||
|
OrthancPlugins::LogError(std::string(request->headersKeys[i]) + " : " + request->headersValues[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string oe2BaseApp = oe2BaseUrl_ + "app/";
|
||||||
|
OrthancPluginRedirect(context, output, &(oe2BaseApp.c_str()[1])); // remove the first '/' to make a relative redirect !
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MergeJson(Json::Value &a, const Json::Value &b) {
|
||||||
|
|
||||||
|
if (!a.isObject() || !b.isObject())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value::Members members = b.getMemberNames();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < members.size(); i++)
|
||||||
|
{
|
||||||
|
std::string key = members[i];
|
||||||
|
|
||||||
|
if (!a[key].isNull() && a[key].type() == Json::objectValue && b[key].type() == Json::objectValue)
|
||||||
|
{
|
||||||
|
MergeJson(a[key], b[key]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// const std::string& val = b[key].asString();
|
||||||
|
a[key] = b[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ReadConfiguration()
|
||||||
|
{
|
||||||
|
orthancFullConfiguration_.reset(new OrthancPlugins::OrthancConfiguration);
|
||||||
|
|
||||||
|
// read default configuration
|
||||||
|
std::string defaultConfigurationFileContent;
|
||||||
|
Orthanc::EmbeddedResources::GetFileResource(defaultConfigurationFileContent, Orthanc::EmbeddedResources::DEFAULT_CONFIGURATION);
|
||||||
|
|
||||||
|
Json::Value defaultConfiguration;
|
||||||
|
OrthancPlugins::ReadJsonWithoutComments(defaultConfiguration, defaultConfigurationFileContent);
|
||||||
|
pluginJsonConfiguration_ = defaultConfiguration["OrthancExplorer2"];
|
||||||
|
|
||||||
|
if (orthancFullConfiguration_->IsSection("OrthancExplorer2"))
|
||||||
|
{
|
||||||
|
OrthancPlugins::OrthancConfiguration pluginConfiguration(false);
|
||||||
|
orthancFullConfiguration_->GetSection(pluginConfiguration, "OrthancExplorer2");
|
||||||
|
|
||||||
|
Json::Value jsonConfig = pluginConfiguration.GetJson();
|
||||||
|
|
||||||
|
// backward compatibility
|
||||||
|
if (jsonConfig.isMember("UiOptions"))
|
||||||
|
{
|
||||||
|
// fix typo from version 0.7.0
|
||||||
|
if (jsonConfig["UiOptions"].isMember("EnableAnonimization") && !jsonConfig["UiOptions"].isMember("EnableAnonymization"))
|
||||||
|
{
|
||||||
|
LOG(WARNING) << "You are still using the 'UiOptions.EnableAnonimization' configuration that has a typo. You should use 'UiOptions.EnableAnonymization' instead.";
|
||||||
|
jsonConfig["UiOptions"]["EnableAnonymization"] = jsonConfig["UiOptions"]["EnableAnonimization"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonConfig["UiOptions"].isMember("StudyListEmptyIfNoSearch") && !jsonConfig["UiOptions"].isMember("StudyListContentIfNoSearch"))
|
||||||
|
{
|
||||||
|
if (jsonConfig["UiOptions"]["StudyListEmptyIfNoSearch"].asBool() == true)
|
||||||
|
{
|
||||||
|
LOG(WARNING) << "You are still using the 'UiOptions.StudyListEmptyIfNoSearch' configuration that is now deprecated a typo. You should use 'UiOptions.StudyListContentIfNoSearch' instead.";
|
||||||
|
jsonConfig["UiOptions"]["StudyListContentIfNoSearch"] = "empty";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openInOhifV3IsExplicitelyDisabled = jsonConfig["UiOptions"].isMember("EnableOpenInOhifViewer3") && jsonConfig["UiOptions"]["EnableOpenInOhifViewer3"].asBool() == false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MergeJson(pluginJsonConfiguration_, jsonConfig);
|
||||||
|
|
||||||
|
if (jsonConfig.isMember("CustomCssPath") && jsonConfig["CustomCssPath"].isString())
|
||||||
|
{
|
||||||
|
customCssPath_ = jsonConfig["CustomCssPath"].asString();
|
||||||
|
if (!Orthanc::SystemToolbox::IsExistingFile(customCssPath_))
|
||||||
|
{
|
||||||
|
LOG(ERROR) << "Unable to accesss the 'CustomCssPath': " << customCssPath_;
|
||||||
|
throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonConfig.isMember("CustomLogoPath") && jsonConfig["CustomLogoPath"].isString())
|
||||||
|
{
|
||||||
|
customLogoPath_ = jsonConfig["CustomLogoPath"].asString();
|
||||||
|
if (!Orthanc::SystemToolbox::IsExistingFile(customLogoPath_))
|
||||||
|
{
|
||||||
|
LOG(ERROR) << "Unable to accesss the 'CustomLogoPath': " << customLogoPath_;
|
||||||
|
throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonConfig.isMember("CustomLogoUrl") && jsonConfig["CustomLogoUrl"].isString())
|
||||||
|
{
|
||||||
|
customLogoUrl_ = jsonConfig["CustomLogoUrl"].asString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonConfig.isMember("Theme") && jsonConfig["Theme"].isString() && jsonConfig["Theme"].asString() == "dark")
|
||||||
|
{
|
||||||
|
theme_ = "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonConfig.isMember("CustomFavIconPath") && jsonConfig["CustomFavIconPath"].isString())
|
||||||
|
{
|
||||||
|
customFavIconPath_ = jsonConfig["CustomFavIconPath"].asString();
|
||||||
|
if (!Orthanc::SystemToolbox::IsExistingFile(customFavIconPath_))
|
||||||
|
{
|
||||||
|
LOG(ERROR) << "Unable to accesss the 'CustomFavIconPath': " << customFavIconPath_;
|
||||||
|
throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonConfig.isMember("CustomTitle") && jsonConfig["CustomTitle"].isString())
|
||||||
|
{
|
||||||
|
customTitle_ = jsonConfig["CustomTitle"].asString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enableShares_ = pluginJsonConfiguration_["UiOptions"]["EnableShares"].asBool(); // we are sure that the value exists since it is in the default configuration file
|
||||||
|
|
||||||
|
isReadOnly_ = orthancFullConfiguration_->GetBooleanValue("ReadOnly", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetPluginConfiguration(Json::Value& jsonPluginConfiguration, const std::string& sectionName)
|
||||||
|
{
|
||||||
|
if (orthancFullConfiguration_->IsSection(sectionName))
|
||||||
|
{
|
||||||
|
OrthancPlugins::OrthancConfiguration pluginConfiguration(false);
|
||||||
|
orthancFullConfiguration_->GetSection(pluginConfiguration, sectionName);
|
||||||
|
|
||||||
|
jsonPluginConfiguration = pluginConfiguration.GetJson();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool IsPluginEnabledInConfiguration(const std::string& sectionName, const std::string& enableValueName, bool defaultValue)
|
||||||
|
{
|
||||||
|
if (orthancFullConfiguration_->IsSection(sectionName))
|
||||||
|
{
|
||||||
|
OrthancPlugins::OrthancConfiguration pluginConfiguration(false);
|
||||||
|
orthancFullConfiguration_->GetSection(pluginConfiguration, sectionName);
|
||||||
|
|
||||||
|
return pluginConfiguration.GetBooleanValue(enableValueName, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value GetPluginInfo(const std::string& pluginName)
|
||||||
|
{
|
||||||
|
Json::Value pluginInfo;
|
||||||
|
|
||||||
|
OrthancPlugins::RestApiGet(pluginInfo, "/plugins/" + pluginName, false);
|
||||||
|
|
||||||
|
return pluginInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value GetKeycloakConfiguration()
|
||||||
|
{
|
||||||
|
if (pluginJsonConfiguration_.isMember("Keycloak"))
|
||||||
|
{
|
||||||
|
const Json::Value& keyCloakSection = pluginJsonConfiguration_["Keycloak"];
|
||||||
|
if (keyCloakSection.isMember("Enable") && keyCloakSection["Enable"].asBool() == true)
|
||||||
|
{
|
||||||
|
return pluginJsonConfiguration_["Keycloak"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json::nullValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value GetPluginsConfiguration(bool& hasUserProfile)
|
||||||
|
{
|
||||||
|
Json::Value pluginsConfiguration;
|
||||||
|
Json::Value pluginList;
|
||||||
|
|
||||||
|
Orthanc::UriComponents components;
|
||||||
|
Orthanc::Toolbox::SplitUriComponents(components, oe2BaseUrl_);
|
||||||
|
std::string pluginUriPrefix = ""; // the RootUri is provided relative to Orthanc Explorer /app/explorer.html -> we need to correct this !
|
||||||
|
for (size_t i = 0; i < components.size(); i++)
|
||||||
|
{
|
||||||
|
pluginUriPrefix += "../";
|
||||||
|
}
|
||||||
|
|
||||||
|
OrthancPlugins::RestApiGet(pluginList, "/plugins", false);
|
||||||
|
|
||||||
|
for (Json::Value::ArrayIndex i = 0; i < pluginList.size(); i++)
|
||||||
|
{
|
||||||
|
Json::Value pluginConfiguration;
|
||||||
|
std::string pluginName = pluginList[i].asString();
|
||||||
|
|
||||||
|
if (pluginName == "explorer.js")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value pluginInfo = GetPluginInfo(pluginName);
|
||||||
|
|
||||||
|
if (pluginInfo.isMember("RootUri") && pluginInfo["RootUri"].asString().size() > 0)
|
||||||
|
{
|
||||||
|
pluginInfo["RootUri"] = pluginUriPrefix + pluginInfo["RootUri"].asString();
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginsConfiguration[pluginName] = pluginInfo;
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = true; // we assume that unknown plugins are enabled (if they are loaded by Orthanc)
|
||||||
|
|
||||||
|
if (pluginName == "authorization")
|
||||||
|
{
|
||||||
|
pluginConfiguration = Json::nullValue;
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "Authorization")
|
||||||
|
&& (pluginConfiguration.isMember("WebService")
|
||||||
|
|| pluginConfiguration.isMember("WebServiceRootUrl")
|
||||||
|
|| pluginConfiguration.isMember("WebServiceUserProfileUrl")
|
||||||
|
|| pluginConfiguration.isMember("WebServiceTokenValidationUrl")
|
||||||
|
|| pluginConfiguration.isMember("WebServiceTokenCreationBaseUrl")
|
||||||
|
|| pluginConfiguration.isMember("WebServiceTokenDecoderUrl"));
|
||||||
|
hasUserProfile = GetPluginConfiguration(pluginConfiguration, "Authorization") && (pluginConfiguration.isMember("WebServiceUserProfileUrl") || pluginConfiguration.isMember("WebServiceRootUrl"));
|
||||||
|
|
||||||
|
if (!pluginConfiguration.isMember("CheckedLevel") || pluginConfiguration["CheckedLevel"].asString() != "studies")
|
||||||
|
{
|
||||||
|
LOG(WARNING) << "When using OE2 and the authorization plugin together, you must set 'Authorization.CheckedLevel' to 'studies'. Unless you are using this orthanc only to generate tokens.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (pluginName == "AWS S3 Storage")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "AwsS3Storage");
|
||||||
|
}
|
||||||
|
else if (pluginName == "Azure Blob Storage")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "AzureBlobStorage");
|
||||||
|
}
|
||||||
|
else if (pluginName == "connectivity-checks")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
||||||
|
}
|
||||||
|
else if (pluginName == "ohif")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
||||||
|
std::string ohifDataSource = "dicom-web";
|
||||||
|
if (GetPluginConfiguration(pluginConfiguration, "OHIF"))
|
||||||
|
{
|
||||||
|
if (pluginConfiguration.isMember("DataSource") && pluginConfiguration["DataSource"].asString() == "dicom-json")
|
||||||
|
{
|
||||||
|
ohifDataSource = "dicom-json";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pluginsConfiguration[pluginName]["DataSource"] = ohifDataSource;
|
||||||
|
}
|
||||||
|
else if (pluginName == "delayed-deletion")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("DelayedDeletion", "Enable", false);
|
||||||
|
}
|
||||||
|
else if (pluginName == "dicom-web")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("DicomWeb", "Enable", false);
|
||||||
|
}
|
||||||
|
else if (pluginName == "gdcm")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Gdcm", "Enable", true);
|
||||||
|
}
|
||||||
|
else if (pluginName == "Google Cloud Storage")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "GoogleCloudStorage");
|
||||||
|
}
|
||||||
|
else if (pluginName == "mysql-index")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("MySQL", "EnableIndex", false);
|
||||||
|
}
|
||||||
|
else if (pluginName == "mysql-storage")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("MySQL", "EnableStorage", false);
|
||||||
|
}
|
||||||
|
else if (pluginName == "odbc-index")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Odbc", "EnableIndex", false);
|
||||||
|
}
|
||||||
|
else if (pluginName == "odbc-storage")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Odbc", "EnableStorage", false);
|
||||||
|
}
|
||||||
|
else if (pluginName == "postgresql-index")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("PostgreSQL", "EnableIndex", false);
|
||||||
|
}
|
||||||
|
else if (pluginName == "postgresql-storage")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("PostgreSQL", "EnableStorage", false);
|
||||||
|
}
|
||||||
|
else if (pluginName == "osimis-web-viewer")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "WebViewer");
|
||||||
|
}
|
||||||
|
else if (pluginName == "python")
|
||||||
|
{
|
||||||
|
std::string notUsed;
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = orthancFullConfiguration_->LookupStringValue(notUsed, "PythonScript");
|
||||||
|
}
|
||||||
|
else if (pluginName == "serve-folders")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = GetPluginConfiguration(pluginConfiguration, "ServeFolders");
|
||||||
|
}
|
||||||
|
else if (pluginName == "stone-webviewer")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
||||||
|
}
|
||||||
|
else if (pluginName == "volview")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
||||||
|
}
|
||||||
|
else if (pluginName == "tcia")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Tcia", "Enable", false);
|
||||||
|
}
|
||||||
|
else if (pluginName == "transfers")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
||||||
|
}
|
||||||
|
else if (pluginName == "web-viewer")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
||||||
|
}
|
||||||
|
else if (pluginName == "worklists")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = IsPluginEnabledInConfiguration("Worklists", "Enable", false);
|
||||||
|
}
|
||||||
|
else if (pluginName == "wsi")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = true;
|
||||||
|
}
|
||||||
|
else if (pluginName == "multitenant-dicom")
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = false;
|
||||||
|
Json::Value pluginConfiguration;
|
||||||
|
if (GetPluginConfiguration(pluginConfiguration, "MultitenantDicom"))
|
||||||
|
{
|
||||||
|
pluginsConfiguration[pluginName]["Enabled"] = pluginConfiguration.isMember("Servers") && pluginConfiguration["Servers"].isArray() && pluginConfiguration["Servers"].size() > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return pluginsConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateUiOptions(Json::Value& uiOption, const std::list<std::string>& permissions, const std::string& anyOfPermissions)
|
||||||
|
{
|
||||||
|
std::vector<std::string> permissionsVector;
|
||||||
|
Orthanc::Toolbox::TokenizeString(permissionsVector, anyOfPermissions, '|');
|
||||||
|
|
||||||
|
bool hasPermission = false;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < permissionsVector.size(); ++i)
|
||||||
|
{
|
||||||
|
hasPermission |= std::find(permissions.begin(), permissions.end(), permissionsVector[i]) != permissions.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
uiOption = uiOption.asBool() && hasPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetOE2Configuration(OrthancPluginRestOutput* output,
|
||||||
|
const char* /*url*/,
|
||||||
|
const OrthancPluginHttpRequest* request)
|
||||||
|
{
|
||||||
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
||||||
|
|
||||||
|
if (request->method != OrthancPluginHttpMethod_Get)
|
||||||
|
{
|
||||||
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Json::Value oe2Configuration;
|
||||||
|
|
||||||
|
oe2Configuration["Plugins"] = pluginsConfiguration_;
|
||||||
|
oe2Configuration["UiOptions"] = pluginJsonConfiguration_["UiOptions"];
|
||||||
|
|
||||||
|
// if OHIF has not been explicitely disabled in the config and if the plugin is loaded, enable it
|
||||||
|
if (!openInOhifV3IsExplicitelyDisabled && pluginsConfiguration_.isMember("ohif"))
|
||||||
|
{
|
||||||
|
oe2Configuration["UiOptions"]["EnableOpenInOhifViewer3"] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value tokens = pluginJsonConfiguration_["Tokens"];
|
||||||
|
if (!tokens.isMember("RequiredForLinks"))
|
||||||
|
{
|
||||||
|
tokens["RequiredForLinks"] = hasUserProfile_;
|
||||||
|
}
|
||||||
|
|
||||||
|
oe2Configuration["Tokens"] = tokens;
|
||||||
|
|
||||||
|
oe2Configuration["HasCustomLogo"] = !customLogoPath_.empty() || !customLogoUrl_.empty();
|
||||||
|
if (!customLogoUrl_.empty())
|
||||||
|
{
|
||||||
|
oe2Configuration["CustomLogoUrl"] = customLogoUrl_;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!customTitle_.empty())
|
||||||
|
{
|
||||||
|
oe2Configuration["CustomTitle"] = customTitle_;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value& uiOptions = oe2Configuration["UiOptions"];
|
||||||
|
|
||||||
|
if (hasUserProfile_)
|
||||||
|
{
|
||||||
|
{// get the available-labels from the auth plugin (and the auth-service)
|
||||||
|
std::map<std::string, std::string> headers;
|
||||||
|
OrthancPlugins::GetHttpHeaders(headers, request);
|
||||||
|
|
||||||
|
uiOptions["EnablePermissionsEdition"] = false;
|
||||||
|
|
||||||
|
Json::Value rolesConfig;
|
||||||
|
if (OrthancPlugins::RestApiGet(rolesConfig, "/auth/settings/roles", headers, true))
|
||||||
|
{
|
||||||
|
if (rolesConfig.isObject() && rolesConfig.isMember("available-labels"))
|
||||||
|
{
|
||||||
|
LOG(INFO) << "Overriding \"AvailableLabels\" in UiOptions with the values from the auth-service";
|
||||||
|
uiOptions["AvailableLabels"] = rolesConfig["available-labels"];
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG(INFO) << rolesConfig.toStyledString();
|
||||||
|
|
||||||
|
// if the auth-service is not fully configured, disable permissions edition
|
||||||
|
if (rolesConfig.isObject() && rolesConfig.isMember("roles") && rolesConfig["roles"].isObject() && rolesConfig["roles"].size() > 0)
|
||||||
|
{
|
||||||
|
uiOptions["EnablePermissionsEdition"] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{// get the user profile from the auth plugin (and the auth-service)
|
||||||
|
std::map<std::string, std::string> headers;
|
||||||
|
OrthancPlugins::GetHttpHeaders(headers, request);
|
||||||
|
|
||||||
|
Json::Value userProfile;
|
||||||
|
OrthancPlugins::RestApiGet(userProfile, "/auth/user/profile", headers, true);
|
||||||
|
|
||||||
|
// modify the UiOptions based on the user profile
|
||||||
|
std::list<std::string> permissions;
|
||||||
|
Orthanc::SerializationToolbox::ReadListOfStrings(permissions, userProfile, "permissions");
|
||||||
|
|
||||||
|
LOG(INFO) << "Overriding \"Enable...\" in UiOptions with the permissions from the auth-service for this user-profile";
|
||||||
|
|
||||||
|
UpdateUiOptions(uiOptions["EnableStudyList"], permissions, "all|view");
|
||||||
|
UpdateUiOptions(uiOptions["EnableViewerQuickButton"], permissions, "all|view");
|
||||||
|
UpdateUiOptions(uiOptions["EnableReportQuickButton"], permissions, "all|view");
|
||||||
|
UpdateUiOptions(uiOptions["EnableUpload"], permissions, "all|upload");
|
||||||
|
UpdateUiOptions(uiOptions["EnableAddSeries"], permissions, "all|upload");
|
||||||
|
UpdateUiOptions(uiOptions["EnableDicomModalities"], permissions, "all|q-r-remote-modalities");
|
||||||
|
UpdateUiOptions(uiOptions["EnableDeleteResources"], permissions, "all|delete");
|
||||||
|
UpdateUiOptions(uiOptions["EnableDownloadZip"], permissions, "all|download");
|
||||||
|
UpdateUiOptions(uiOptions["EnableDownloadDicomDir"], permissions, "all|download");
|
||||||
|
UpdateUiOptions(uiOptions["EnableDownloadDicomFile"], permissions, "all|download");
|
||||||
|
UpdateUiOptions(uiOptions["EnableModification"], permissions, "all|modify");
|
||||||
|
UpdateUiOptions(uiOptions["EnableAnonymization"], permissions, "all|anonymize");
|
||||||
|
UpdateUiOptions(uiOptions["EnableSendTo"], permissions, "all|send");
|
||||||
|
UpdateUiOptions(uiOptions["EnableApiViewMenu"], permissions, "all|api-view");
|
||||||
|
UpdateUiOptions(uiOptions["EnableSettings"], permissions, "all|settings");
|
||||||
|
UpdateUiOptions(uiOptions["EnableShares"], permissions, "all|share");
|
||||||
|
UpdateUiOptions(uiOptions["EnableEditLabels"], permissions, "all|edit-labels");
|
||||||
|
UpdateUiOptions(uiOptions["EnablePermissionsEdition"], permissions, "admin-permissions");
|
||||||
|
|
||||||
|
// the Legacy UI is not available with user profile since it would not refresh the tokens
|
||||||
|
uiOptions["EnableLinkToLegacyUi"] = false;
|
||||||
|
|
||||||
|
oe2Configuration["Profile"] = userProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable operations on read only systems
|
||||||
|
if (isReadOnly_)
|
||||||
|
{
|
||||||
|
uiOptions["EnableUpload"] = false;
|
||||||
|
uiOptions["EnableAddSeries"] = false;
|
||||||
|
uiOptions["EnableDeleteResources"] = false;
|
||||||
|
uiOptions["EnableModification"] = false;
|
||||||
|
uiOptions["EnableAnonymization"] = false;
|
||||||
|
uiOptions["EnableEditLabels"] = false;
|
||||||
|
uiOptions["EnablePermissionsEdition"] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
oe2Configuration["Keycloak"] = GetKeycloakConfiguration();
|
||||||
|
std::string answer = oe2Configuration.toStyledString();
|
||||||
|
OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetOE2PreLoginConfiguration(OrthancPluginRestOutput* output,
|
||||||
|
const char* /*url*/,
|
||||||
|
const OrthancPluginHttpRequest* request)
|
||||||
|
{
|
||||||
|
OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
|
||||||
|
|
||||||
|
if (request->method != OrthancPluginHttpMethod_Get)
|
||||||
|
{
|
||||||
|
OrthancPluginSendMethodNotAllowed(context, output, "GET");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Json::Value oe2Configuration;
|
||||||
|
oe2Configuration["Keycloak"] = GetKeycloakConfiguration();
|
||||||
|
|
||||||
|
std::string answer = oe2Configuration.toStyledString();
|
||||||
|
OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool DisplayPerformanceWarning(OrthancPluginContext* context)
|
||||||
|
{
|
||||||
|
(void) DisplayPerformanceWarning; // Disable warning about unused function
|
||||||
|
OrthancPluginLogWarning(context, "Performance warning in Orthanc Explorer 2: "
|
||||||
|
"Non-release build, runtime debug assertions are turned on");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void CheckRootUrlIsValid(const std::string& value, const std::string& name, bool allowEmpty)
|
||||||
|
{
|
||||||
|
if (allowEmpty && value.size() == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.size() < 1 ||
|
||||||
|
value[0] != '/' ||
|
||||||
|
value[value.size() - 1] != '/')
|
||||||
|
{
|
||||||
|
OrthancPlugins::LogError("Orthanc-Explorer 2: '" + name + "' configuration shall start with a '/' and end with a '/': " + value);
|
||||||
|
throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType,
|
||||||
|
OrthancPluginResourceType resourceType,
|
||||||
|
const char* resourceId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (changeType == OrthancPluginChangeType_OrthancStarted)
|
||||||
|
{
|
||||||
|
// this can not be performed during plugin initialization because it is accessing the DB -> must be done when Orthanc has just started
|
||||||
|
pluginsConfiguration_ = GetPluginsConfiguration(hasUserProfile_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Orthanc::OrthancException& e)
|
||||||
|
{
|
||||||
|
LOG(ERROR) << "Exception: " << e.What();
|
||||||
|
return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return OrthancPluginErrorCode_Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
|
||||||
|
{
|
||||||
|
assert(DisplayPerformanceWarning(context));
|
||||||
|
|
||||||
|
OrthancPlugins::SetGlobalContext(context);
|
||||||
|
|
||||||
|
Orthanc::Logging::InitializePluginContext(context);
|
||||||
|
|
||||||
|
Orthanc::Logging::EnableInfoLevel(true);
|
||||||
|
|
||||||
|
|
||||||
|
/* Check the version of the Orthanc core */
|
||||||
|
if (!OrthancPlugins::CheckMinimalOrthancVersion(ORTHANC_CORE_MINIMAL_MAJOR,
|
||||||
|
ORTHANC_CORE_MINIMAL_MINOR,
|
||||||
|
ORTHANC_CORE_MINIMAL_REVISION))
|
||||||
|
{
|
||||||
|
OrthancPlugins::ReportMinimalOrthancVersion(ORTHANC_CORE_MINIMAL_MAJOR,
|
||||||
|
ORTHANC_CORE_MINIMAL_MINOR,
|
||||||
|
ORTHANC_CORE_MINIMAL_REVISION);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
OrthancPlugins::SetDescription(ORTHANC_PLUGIN_NAME, "Advanced User Interface for Orthanc");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ReadConfiguration();
|
||||||
|
|
||||||
|
if (pluginJsonConfiguration_["Enable"].asBool())
|
||||||
|
{
|
||||||
|
oe2BaseUrl_ = pluginJsonConfiguration_["Root"].asString();
|
||||||
|
|
||||||
|
CheckRootUrlIsValid(oe2BaseUrl_, "Root", false);
|
||||||
|
|
||||||
|
OrthancPlugins::LogWarning("Root URI to the Orthanc-Explorer 2 application: " + oe2BaseUrl_);
|
||||||
|
|
||||||
|
|
||||||
|
OrthancPlugins::RegisterRestCallback
|
||||||
|
<ServeCustomCss>
|
||||||
|
(oe2BaseUrl_ + "app/customizable/custom.css", true);
|
||||||
|
|
||||||
|
if (!customLogoPath_.empty())
|
||||||
|
{
|
||||||
|
OrthancPlugins::RegisterRestCallback
|
||||||
|
<ServeCustomFile<CustomFilesPath_Logo> >
|
||||||
|
(oe2BaseUrl_ + "app/customizable/custom-logo", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to mix the "routing" between the server and the frontend (vue-router)
|
||||||
|
// first part are the files that are 'static files' that must be served by the backend
|
||||||
|
OrthancPlugins::RegisterRestCallback
|
||||||
|
<ServeEmbeddedFolder<Orthanc::EmbeddedResources::WEB_APPLICATION_ASSETS> >
|
||||||
|
(oe2BaseUrl_ + "app/assets/(.*)", true);
|
||||||
|
OrthancPlugins::RegisterRestCallback
|
||||||
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX, Orthanc::MimeType_Html> >
|
||||||
|
(oe2BaseUrl_ + "app/index.html", true);
|
||||||
|
OrthancPlugins::RegisterRestCallback
|
||||||
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX_LANDING, Orthanc::MimeType_Html> >
|
||||||
|
(oe2BaseUrl_ + "app/token-landing.html", true);
|
||||||
|
OrthancPlugins::RegisterRestCallback
|
||||||
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX_RETRIEVE_AND_VIEW, Orthanc::MimeType_Html> >
|
||||||
|
(oe2BaseUrl_ + "app/retrieve-and-view.html", true);
|
||||||
|
|
||||||
|
if (customFavIconPath_.empty())
|
||||||
|
{
|
||||||
|
OrthancPlugins::RegisterRestCallback
|
||||||
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_FAVICON, Orthanc::MimeType_Ico> >
|
||||||
|
(oe2BaseUrl_ + "app/favicon.ico", true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OrthancPlugins::RegisterRestCallback
|
||||||
|
<ServeCustomFile<CustomFilesPath_FavIcon> >
|
||||||
|
(oe2BaseUrl_ + "app/favicon.ico", true);
|
||||||
|
}
|
||||||
|
// second part are all the routes that are actually handled by vue-router and that are actually returning the same file (index.html)
|
||||||
|
OrthancPlugins::RegisterRestCallback
|
||||||
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX, Orthanc::MimeType_Html> >
|
||||||
|
(oe2BaseUrl_ + "app/(.*)", true);
|
||||||
|
OrthancPlugins::RegisterRestCallback
|
||||||
|
<ServeEmbeddedFile<Orthanc::EmbeddedResources::WEB_APPLICATION_INDEX, Orthanc::MimeType_Html> >
|
||||||
|
(oe2BaseUrl_ + "app", true);
|
||||||
|
|
||||||
|
OrthancPlugins::RegisterRestCallback<GetOE2Configuration>(oe2BaseUrl_ + "api/configuration", true);
|
||||||
|
OrthancPlugins::RegisterRestCallback<GetOE2PreLoginConfiguration>(oe2BaseUrl_ + "api/pre-login-configuration", true);
|
||||||
|
|
||||||
|
std::string pluginRootUri = oe2BaseUrl_ + "app/";
|
||||||
|
OrthancPlugins::SetRootUri(ORTHANC_PLUGIN_NAME, pluginRootUri.c_str());
|
||||||
|
|
||||||
|
if (pluginJsonConfiguration_["IsDefaultOrthancUI"].asBool())
|
||||||
|
{
|
||||||
|
OrthancPlugins::RegisterRestCallback<RedirectRoot>("/", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::string explorer;
|
||||||
|
Orthanc::EmbeddedResources::GetFileResource(explorer, Orthanc::EmbeddedResources::ORTHANC_EXPLORER);
|
||||||
|
|
||||||
|
std::map<std::string, std::string> dictionary;
|
||||||
|
dictionary["OE2_BASE_URL"] = oe2BaseUrl_.substr(1, oe2BaseUrl_.size() - 2); // Remove heading and trailing slashes
|
||||||
|
std::string explorerConfigured = Orthanc::Toolbox::SubstituteVariables(explorer, dictionary);
|
||||||
|
|
||||||
|
OrthancPluginExtendOrthancExplorer(OrthancPlugins::GetGlobalContext(), explorerConfigured.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OrthancPlugins::LogWarning("Orthanc Explorer 2 plugin is disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Orthanc::OrthancException& e)
|
||||||
|
{
|
||||||
|
OrthancPlugins::LogError("Exception while initializing the Orthanc-Explorer 2 plugin: " +
|
||||||
|
std::string(e.What()));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
OrthancPlugins::LogError("Exception while initializing the Orthanc-Explorer 2 plugin");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ORTHANC_PLUGINS_API void OrthancPluginFinalize()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ORTHANC_PLUGINS_API const char* OrthancPluginGetName()
|
||||||
|
{
|
||||||
|
return ORTHANC_PLUGIN_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ORTHANC_PLUGINS_API const char* OrthancPluginGetVersion()
|
||||||
|
{
|
||||||
|
return ORTHANC_OE2_VERSION;
|
||||||
|
}
|
||||||
|
}
|
||||||
148
README.md
Normal file
148
README.md
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
# Orthanc Explorer 2
|
||||||
|
|
||||||
|
This [Orthanc](https://www.orthanc-server.com) plugin implements a new
|
||||||
|
user interface for Orthanc to replace the Orthanc Explorer.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Binaries are available:
|
||||||
|
- [here on Github](https://github.com/orthanc-server/orthanc-explorer-2/releases), you'll find the plugin for
|
||||||
|
- Windows 64bits
|
||||||
|
- Ubuntu 20.04
|
||||||
|
- MacOS (Universal binary)
|
||||||
|
- in the [Windows Installers](https://orthanc.uclouvain.be/downloads/windows-64/installers/index.html) (64bits version only),
|
||||||
|
the plugin is enabled but, right now, the legacy Orthanc Explorer
|
||||||
|
remains the default UI. Note that, in a future release, OE2 will become the default UI for Orthanc in the Windows Installers.
|
||||||
|
- in the [orthancteam/orthanc](https://hub.docker.com/r/orthancteam/orthanc) Docker images,
|
||||||
|
the plugin is enabled but, right now, the legacy Orthanc Explorer
|
||||||
|
remains the default UI. Note that, in a future release, OE2 will become the default UI for Orthanc in the Windows Installers.
|
||||||
|
|
||||||
|
The binaries must be copied next to other Orthanc plugins (Windows: `C:\\Program Files\\Orthanc Server\\Plugins`, ubuntu: `/usr/share/orthanc/plugins/`). You should also possible make sure that the plugin is listed in the `"Plugins"` Orthanc configuration.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Like any other Orthanc plugins, Orthanc Explorer 2 is configured through
|
||||||
|
a json [configuration file](Plugin/DefaultConfiguration.json) that is provided to Orthanc at startup.
|
||||||
|
|
||||||
|
At minimum, you should provide this configuration file:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Plugins": [ "." ],
|
||||||
|
"OrthancExplorer2": {
|
||||||
|
"Enable": true,
|
||||||
|
"IsDefaultOrthancUI": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Using this minimal configuration file, Orthanc Explorer 2 is
|
||||||
|
accessible at http://localhost:8042/ui/app/ . If `IsDefaultOrthancUI`
|
||||||
|
is set to `true`, Orthanc Explorer 2 will replace the built-in Orthanc
|
||||||
|
Explorer.
|
||||||
|
|
||||||
|
If you are using Docker, the easiest way to try the new Orthanc Explorer 2 is to run this command and then open a browser in http://localhost:8042/ui/app/ (login: orthanc, pwd: orthanc)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker pull orthancteam/orthanc:latest
|
||||||
|
docker run -p 8042:8042 orthancteam/orthanc:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Front-end development
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
- install `nodejs` version 14 or higher and `npm` version 6 or higher
|
||||||
|
- install nginx
|
||||||
|
|
||||||
|
Then, to continuously build and serve the front-end code on your machine, in a shell, type:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd WebApplication
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
Npm will then serve the `/ui/app/` static code (HTML/JS).
|
||||||
|
|
||||||
|
Then, launch an Orthanc with the OE2 plugin already enabled and listening on localhost:8043. This can be achieved with by typing this command in another shell:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -p 8043:8042 -e ORTHANC__AUTHENTICATION_ENABLED=false orthancteam/orthanc:24.6.2
|
||||||
|
```
|
||||||
|
This Orthanc will serve the `/ui/api/` routes.
|
||||||
|
|
||||||
|
In third shell, type:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo ./scripts/start-nginx.sh
|
||||||
|
```
|
||||||
|
This will launch an nginx server that will implement reverse proxies to serve both the static code and the Orthanc Rest API on a single endpoind.
|
||||||
|
You may then open http://localhost:9999/ui/app/ to view and debug your current front-end code. As soon as you modify a `WebApplication` source file, the UI shall reload automatically in the browser.
|
||||||
|
|
||||||
|
Edit scripts/nginx-dev.conf if needed.
|
||||||
|
|
||||||
|
|
||||||
|
## Compilation
|
||||||
|
|
||||||
|
Prerequisites to build the frontend: you need `nodejs` version 14 or higher and `npm` version 6 or higher.
|
||||||
|
|
||||||
|
To build the frontend:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd WebApplication
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
And then, to build the C++ plugin:
|
||||||
|
```
|
||||||
|
cd /build
|
||||||
|
cmake -DWEBAPP_DIST_SOURCE=LOCAL -DALLOW_DOWNLOADS=ON -DCMAKE_BUILD_TYPE:STRING=Release -DUSE_SYSTEM_ORTHANC_SDK=OFF /sources/orthanc-explorer-2/
|
||||||
|
make -j 4
|
||||||
|
```
|
||||||
|
|
||||||
|
### LSB (Linux Standard Base)
|
||||||
|
|
||||||
|
Here are the build instructions for LSB:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd WebApplication
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
mkdir build-lsb
|
||||||
|
cd build-lsb
|
||||||
|
LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake -DCMAKE_TOOLCHAIN_FILE=../Resources/Orthanc/Toolchains/LinuxStandardBaseToolchain.cmake -DALLOW_DOWNLOADS=ON -DSTATIC_BUILD=ON -DUSE_LEGACY_JSONCPP=ON -DWEBAPP_DIST_SOURCE=WEB ..
|
||||||
|
make -j4
|
||||||
|
```
|
||||||
|
|
||||||
|
Pre-compiled LSB binaries can be found at: https://orthanc.uclouvain.be/downloads/linux-standard-base/orthanc-explorer-2/index.html
|
||||||
|
|
||||||
|
### Linking against system-wide Orthanc Framework
|
||||||
|
|
||||||
|
Here are the build instructions to dynamic link against the
|
||||||
|
system-wide Orthanc Framework:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd WebApplication
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
mkdir build-system
|
||||||
|
cd build-system
|
||||||
|
cmake .. -DCMAKE_BUILD_TYPE=Debug -DORTHANC_FRAMEWORK_SOURCE=system -DUSE_SYSTEM_GOOGLE_TEST=OFF -DALLOW_DOWNLOADS=ON -DUSE_SYSTEM_ORTHANC_SDK=OFF -DWEBAPP_DIST_SOURCE=LOCAL
|
||||||
|
make -j4
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Releasing
|
||||||
|
|
||||||
|
```
|
||||||
|
git tag -a 0.2.0 -m 0.2.0
|
||||||
|
git push --tags
|
||||||
|
git push
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributions
|
||||||
|
|
||||||
|
Feel free to fork this project, modify it and submit pull requests.
|
||||||
55
Resources/CreateDistPackage.sh
Normal file
55
Resources/CreateDistPackage.sh
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2024 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
|
||||||
|
|
||||||
|
|
||||||
|
# This command-line script uses the "npm" tool to populate the "dist"
|
||||||
|
# folder of Orthanc Explorer 2. It uses Docker to this end, in order
|
||||||
|
# to be usable on our CIS.
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
if [ -t 1 ]; then
|
||||||
|
# TTY is available => use interactive mode
|
||||||
|
DOCKER_FLAGS='-i'
|
||||||
|
fi
|
||||||
|
|
||||||
|
ROOT_DIR=`dirname $(readlink -f $0)`/..
|
||||||
|
IMAGE=orthanc-explorer-2-npm
|
||||||
|
|
||||||
|
echo "Creating the distribution of Orthanc Explorer 2"
|
||||||
|
|
||||||
|
if [ -e "${ROOT_DIR}/WebApplication/dist/" ]; then
|
||||||
|
echo "Target folder is already existing, aborting"
|
||||||
|
exit -1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ${ROOT_DIR}/WebApplication/dist/
|
||||||
|
|
||||||
|
( cd ${ROOT_DIR}/Resources/CreateDistPackage && \
|
||||||
|
docker build --no-cache -t ${IMAGE} . )
|
||||||
|
|
||||||
|
docker run -t ${DOCKER_FLAGS} --rm \
|
||||||
|
--user $(id -u):$(id -g) \
|
||||||
|
-v ${ROOT_DIR}/Resources/CreateDistPackage/build.sh:/source/build.sh:ro \
|
||||||
|
-v ${ROOT_DIR}/WebApplication:/source/WebApplication:ro \
|
||||||
|
-v ${ROOT_DIR}/WebApplication/dist/:/target:rw \
|
||||||
|
${IMAGE} \
|
||||||
|
bash /source/build.sh
|
||||||
28
Resources/CreateDistPackage/Dockerfile
Normal file
28
Resources/CreateDistPackage/Dockerfile
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2022 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2021-2022 Sebastien Jodogne, ICTEAM UCLouvain, 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/>.
|
||||||
|
|
||||||
|
|
||||||
|
FROM node:19.7.0-bullseye-slim
|
||||||
|
|
||||||
|
MAINTAINER Sebastien Jodogne <s.jodogne@gmail.com>
|
||||||
|
LABEL Description="Orthanc, free DICOM server" Vendor="The Orthanc project"
|
||||||
|
|
||||||
|
RUN apt-get -y clean && apt-get -y update && \
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get -y install patch && \
|
||||||
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
30
Resources/CreateDistPackage/build.sh
Normal file
30
Resources/CreateDistPackage/build.sh
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2024 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, 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 -ex
|
||||||
|
|
||||||
|
cp -r /source/WebApplication /tmp/
|
||||||
|
|
||||||
|
cd /tmp/WebApplication
|
||||||
|
npm install --cache /tmp/npm-cache
|
||||||
|
npm run build --cache /tmp/npm-cache
|
||||||
|
|
||||||
|
cp -r /tmp/WebApplication/dist/* /target
|
||||||
38
Resources/MoveTranslations.py
Normal file
38
Resources/MoveTranslations.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
here = pathlib.Path(__file__).parent.resolve() / "../WebApplication/src/locales/"
|
||||||
|
|
||||||
|
for p in here.glob("*.json"):
|
||||||
|
with open(p, "r", encoding="utf-8") as f:
|
||||||
|
translations = json.load(f)
|
||||||
|
|
||||||
|
if "settings" in translations and isinstance(translations["settings"], str):
|
||||||
|
title = translations["settings"]
|
||||||
|
del translations["settings"]
|
||||||
|
translations["settings"] = {}
|
||||||
|
|
||||||
|
for i in ["dicom_AET",
|
||||||
|
"dicom_port",
|
||||||
|
"ingest_transcoding",
|
||||||
|
"installed_plugins",
|
||||||
|
"orthanc_name",
|
||||||
|
"orthanc_system_info",
|
||||||
|
"orthanc_version",
|
||||||
|
"overwrite_instances",
|
||||||
|
"plugins_not_enabled",
|
||||||
|
"striked_through",
|
||||||
|
"ingest_transcoding",
|
||||||
|
"verbosity_level",
|
||||||
|
"storage_compression",
|
||||||
|
"storage_size",
|
||||||
|
"statistics"
|
||||||
|
]:
|
||||||
|
if i in translations:
|
||||||
|
translations["settings"][i] = translations[i]
|
||||||
|
del translations[i]
|
||||||
|
|
||||||
|
with open(p, "w", encoding="utf-8") as f:
|
||||||
|
data = json.dump(translations, f, ensure_ascii=False, indent=4, sort_keys=True)
|
||||||
80
Resources/Orthanc/CMake/AutoGeneratedCode.cmake
Normal file
80
Resources/Orthanc/CMake/AutoGeneratedCode.cmake
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
set(EMBED_RESOURCES_PYTHON "${CMAKE_CURRENT_LIST_DIR}/../EmbedResources.py"
|
||||||
|
CACHE INTERNAL "Path to the EmbedResources.py script from Orthanc")
|
||||||
|
set(AUTOGENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/AUTOGENERATED")
|
||||||
|
set(AUTOGENERATED_SOURCES)
|
||||||
|
|
||||||
|
file(MAKE_DIRECTORY ${AUTOGENERATED_DIR})
|
||||||
|
include_directories(${AUTOGENERATED_DIR})
|
||||||
|
|
||||||
|
macro(EmbedResources)
|
||||||
|
# Convert a semicolon separated list to a whitespace separated string
|
||||||
|
set(SCRIPT_OPTIONS)
|
||||||
|
set(SCRIPT_ARGUMENTS)
|
||||||
|
set(DEPENDENCIES)
|
||||||
|
set(IS_PATH_NAME false)
|
||||||
|
|
||||||
|
set(TARGET_BASE "${AUTOGENERATED_DIR}/EmbeddedResources")
|
||||||
|
|
||||||
|
# Loop over the arguments of the function
|
||||||
|
foreach(arg ${ARGN})
|
||||||
|
# Extract the first character of the argument
|
||||||
|
string(SUBSTRING "${arg}" 0 1 FIRST_CHAR)
|
||||||
|
if (${FIRST_CHAR} STREQUAL "-")
|
||||||
|
# If the argument starts with a dash "-", this is an option to
|
||||||
|
# EmbedResources.py
|
||||||
|
if (${arg} MATCHES "--target=.*")
|
||||||
|
# Does the argument starts with "--target="?
|
||||||
|
string(SUBSTRING "${arg}" 9 -1 TARGET) # 9 is the length of "--target="
|
||||||
|
set(TARGET_BASE "${AUTOGENERATED_DIR}/${TARGET}")
|
||||||
|
else()
|
||||||
|
list(APPEND SCRIPT_OPTIONS ${arg})
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
if (${IS_PATH_NAME})
|
||||||
|
list(APPEND SCRIPT_ARGUMENTS "${arg}")
|
||||||
|
list(APPEND DEPENDENCIES "${arg}")
|
||||||
|
set(IS_PATH_NAME false)
|
||||||
|
else()
|
||||||
|
list(APPEND SCRIPT_ARGUMENTS "${arg}")
|
||||||
|
set(IS_PATH_NAME true)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT
|
||||||
|
"${TARGET_BASE}.h"
|
||||||
|
"${TARGET_BASE}.cpp"
|
||||||
|
COMMAND ${PYTHON_EXECUTABLE} ${EMBED_RESOURCES_PYTHON}
|
||||||
|
${SCRIPT_OPTIONS} "${TARGET_BASE}" ${SCRIPT_ARGUMENTS}
|
||||||
|
DEPENDS
|
||||||
|
${EMBED_RESOURCES_PYTHON}
|
||||||
|
${DEPENDENCIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
list(APPEND AUTOGENERATED_SOURCES
|
||||||
|
"${TARGET_BASE}.cpp"
|
||||||
|
)
|
||||||
|
endmacro()
|
||||||
303
Resources/Orthanc/CMake/Compiler.cmake
Normal file
303
Resources/Orthanc/CMake/Compiler.cmake
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
# This file sets all the compiler-related flags
|
||||||
|
|
||||||
|
|
||||||
|
# Save the current compiler flags to the cache every time cmake configures the project
|
||||||
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}" CACHE STRING "compiler flags" FORCE)
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}" CACHE STRING "compiler flags" FORCE)
|
||||||
|
|
||||||
|
|
||||||
|
include(CheckLibraryExists)
|
||||||
|
|
||||||
|
if ((CMAKE_CROSSCOMPILING AND NOT
|
||||||
|
"${CMAKE_SYSTEM_VERSION}" STREQUAL "CrossToolNg") OR
|
||||||
|
"${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
|
||||||
|
# Cross-compilation necessarily implies standalone and static build
|
||||||
|
SET(STATIC_BUILD ON)
|
||||||
|
SET(STANDALONE_BUILD ON)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
|
||||||
|
# Cache the environment variables "LSB_CC" and "LSB_CXX" for further
|
||||||
|
# use by "ExternalProject" in CMake
|
||||||
|
SET(CMAKE_LSB_CC $ENV{LSB_CC} CACHE STRING "")
|
||||||
|
SET(CMAKE_LSB_CXX $ENV{LSB_CXX} CACHE STRING "")
|
||||||
|
|
||||||
|
# This is necessary to build "Orthanc mainline - Framework LSB
|
||||||
|
# Release" on "buildbot-worker-debian11"
|
||||||
|
set(LSB_PTHREAD_NONSHARED "${LSB_PATH}/lib64-${LSB_TARGET_VERSION}/libpthread_nonshared.a")
|
||||||
|
if (EXISTS ${LSB_PTHREAD_NONSHARED})
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${LSB_PTHREAD_NONSHARED}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
if (CMAKE_COMPILER_IS_GNUCXX)
|
||||||
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wno-long-long")
|
||||||
|
|
||||||
|
# --std=c99 makes libcurl not to compile
|
||||||
|
# -pedantic gives a lot of warnings on OpenSSL
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-long-long -Wno-variadic-macros")
|
||||||
|
|
||||||
|
if (CMAKE_CROSSCOMPILING)
|
||||||
|
# http://stackoverflow.com/a/3543845/881731
|
||||||
|
set(CMAKE_RC_COMPILE_OBJECT "<CMAKE_RC_COMPILER> -O coff -I<CMAKE_CURRENT_SOURCE_DIR> <SOURCE> <OBJECT>")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
elseif (MSVC)
|
||||||
|
# Use static runtime under Visual Studio
|
||||||
|
# http://www.cmake.org/Wiki/CMake_FAQ#Dynamic_Replace
|
||||||
|
# http://stackoverflow.com/a/6510446
|
||||||
|
foreach(flag_var
|
||||||
|
CMAKE_C_FLAGS_DEBUG
|
||||||
|
CMAKE_CXX_FLAGS_DEBUG
|
||||||
|
CMAKE_C_FLAGS_RELEASE
|
||||||
|
CMAKE_CXX_FLAGS_RELEASE
|
||||||
|
CMAKE_C_FLAGS_MINSIZEREL
|
||||||
|
CMAKE_CXX_FLAGS_MINSIZEREL
|
||||||
|
CMAKE_C_FLAGS_RELWITHDEBINFO
|
||||||
|
CMAKE_CXX_FLAGS_RELWITHDEBINFO)
|
||||||
|
string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
|
||||||
|
string(REGEX REPLACE "/MDd" "/MTd" ${flag_var} "${${flag_var}}")
|
||||||
|
endforeach(flag_var)
|
||||||
|
|
||||||
|
# Add /Zm256 compiler option to Visual Studio to fix PCH errors
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zm256")
|
||||||
|
|
||||||
|
# New in Orthanc 1.5.5
|
||||||
|
if (MSVC_MULTIPLE_PROCESSES)
|
||||||
|
# "If you omit the processMax argument in the /MP option, the
|
||||||
|
# compiler obtains the number of effective processors from the
|
||||||
|
# operating system, and then creates one process per effective
|
||||||
|
# processor"
|
||||||
|
# https://blog.kitware.com/cmake-building-with-all-your-cores/
|
||||||
|
# https://docs.microsoft.com/en-us/cpp/build/reference/mp-build-with-multiple-processes
|
||||||
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP")
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_definitions(
|
||||||
|
-D_CRT_SECURE_NO_WARNINGS=1
|
||||||
|
-D_CRT_SECURE_NO_DEPRECATE=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if (MSVC_VERSION LESS 1600)
|
||||||
|
# Starting with Visual Studio >= 2010 (i.e. macro _MSC_VER >=
|
||||||
|
# 1600), Microsoft ships a standard-compliant <stdint.h>
|
||||||
|
# header. For earlier versions of Visual Studio, give access to a
|
||||||
|
# compatibility header.
|
||||||
|
# http://stackoverflow.com/a/70630/881731
|
||||||
|
# https://en.wikibooks.org/wiki/C_Programming/C_Reference/stdint.h#External_links
|
||||||
|
include_directories(${CMAKE_CURRENT_LIST_DIR}/../../Resources/ThirdParty/VisualStudio)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
link_libraries(netapi32)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
|
||||||
|
${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
|
||||||
|
# In FreeBSD/OpenBSD, the "/usr/local/" folder contains the ports and need to be imported
|
||||||
|
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I/usr/local/include")
|
||||||
|
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -I/usr/local/include")
|
||||||
|
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -L/usr/local/lib")
|
||||||
|
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -L/usr/local/lib")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
|
||||||
|
${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
|
||||||
|
${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
|
||||||
|
${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
|
||||||
|
|
||||||
|
if (# NOT ${CMAKE_SYSTEM_VERSION} STREQUAL "LinuxStandardBase" AND
|
||||||
|
NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD" AND
|
||||||
|
NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
|
||||||
|
# The "--no-undefined" linker flag makes the shared libraries
|
||||||
|
# (plugins ModalityWorklists and ServeFolders) fail to compile on
|
||||||
|
# OpenBSD, and make the PostgreSQL plugin complain about missing
|
||||||
|
# "environ" global variable in FreeBSD.
|
||||||
|
#
|
||||||
|
# TODO - Furthermore, on Linux Standard Base running on Debian 12,
|
||||||
|
# the "-Wl,--no-undefined" seems to break the compilation (added
|
||||||
|
# after Orthanc 1.12.2). This is disabled for now.
|
||||||
|
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Remove the "-rdynamic" option
|
||||||
|
# http://www.mail-archive.com/cmake@cmake.org/msg08837.html
|
||||||
|
set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "")
|
||||||
|
link_libraries(pthread)
|
||||||
|
|
||||||
|
if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
|
||||||
|
link_libraries(rt)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND
|
||||||
|
NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
|
||||||
|
link_libraries(dl)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND
|
||||||
|
NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
|
||||||
|
# The "--as-needed" linker flag is not available on FreeBSD and OpenBSD
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--as-needed")
|
||||||
|
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--as-needed")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--as-needed")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" AND
|
||||||
|
NOT ${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
|
||||||
|
# FreeBSD/OpenBSD have just one single interface for file
|
||||||
|
# handling, which is 64bit clean, so there is no need to define macro
|
||||||
|
# for LFS (Large File Support).
|
||||||
|
# https://ohse.de/uwe/articles/lfs.html
|
||||||
|
add_definitions(
|
||||||
|
-D_LARGEFILE64_SOURCE=1
|
||||||
|
-D_FILE_OFFSET_BITS=64
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
|
||||||
|
if (MSVC)
|
||||||
|
message("MSVC compiler version = " ${MSVC_VERSION} "\n")
|
||||||
|
# Starting Visual Studio 2013 (version 1800), it is not possible
|
||||||
|
# to target Windows XP anymore
|
||||||
|
if (MSVC_VERSION LESS 1800)
|
||||||
|
add_definitions(
|
||||||
|
-DWINVER=0x0501
|
||||||
|
-D_WIN32_WINNT=0x0501
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
else()
|
||||||
|
add_definitions(
|
||||||
|
-DWINVER=0x0501
|
||||||
|
-D_WIN32_WINNT=0x0501
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_definitions(
|
||||||
|
-D_CRT_SECURE_NO_WARNINGS=1
|
||||||
|
)
|
||||||
|
link_libraries(rpcrt4 ws2_32 iphlpapi) # "iphlpapi" is for "SystemToolbox::GetMacAddresses()"
|
||||||
|
|
||||||
|
if (CMAKE_COMPILER_IS_GNUCXX)
|
||||||
|
# Some additional C/C++ compiler flags for MinGW
|
||||||
|
SET(MINGW_NO_WARNINGS "-Wno-unused-function -Wno-unused-variable")
|
||||||
|
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${MINGW_NO_WARNINGS} -Wno-pointer-to-int-cast -Wno-int-to-pointer-cast")
|
||||||
|
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${MINGW_NO_WARNINGS}")
|
||||||
|
|
||||||
|
if (DYNAMIC_MINGW_STDLIB)
|
||||||
|
else()
|
||||||
|
# This is a patch for MinGW64
|
||||||
|
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--allow-multiple-definition -static-libgcc -static-libstdc++")
|
||||||
|
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--allow-multiple-definition -static-libgcc -static-libstdc++")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
CHECK_LIBRARY_EXISTS(winpthread pthread_create "" HAVE_WIN_PTHREAD)
|
||||||
|
if (HAVE_WIN_PTHREAD)
|
||||||
|
if (DYNAMIC_MINGW_STDLIB)
|
||||||
|
else()
|
||||||
|
# This line is necessary to compile with recent versions of MinGW,
|
||||||
|
# otherwise "libwinpthread-1.dll" is not statically linked.
|
||||||
|
SET(CMAKE_CXX_STANDARD_LIBRARIES "${CMAKE_CXX_STANDARD_LIBRARIES} -Wl,-Bstatic -lstdc++ -lpthread -Wl,-Bdynamic")
|
||||||
|
endif()
|
||||||
|
add_definitions(-DHAVE_WIN_PTHREAD=1)
|
||||||
|
else()
|
||||||
|
add_definitions(-DHAVE_WIN_PTHREAD=0)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
|
||||||
|
|
||||||
|
# fix this error that appears with recent compilers on MacOS: boost/mpl/aux_/integral_wrapper.hpp:73:31: error: integer value -1 is outside the valid range of values [0, 3] for this enumeration type [-Wenum-constexpr-conversion]
|
||||||
|
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-enum-constexpr-conversion")
|
||||||
|
|
||||||
|
add_definitions(
|
||||||
|
-D_XOPEN_SOURCE=1
|
||||||
|
)
|
||||||
|
link_libraries(iconv)
|
||||||
|
|
||||||
|
elseif (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
|
||||||
|
message("Building using Emscripten (for WebAssembly or asm.js targets)")
|
||||||
|
include(${CMAKE_CURRENT_LIST_DIR}/EmscriptenParameters.cmake)
|
||||||
|
|
||||||
|
elseif (CMAKE_SYSTEM_NAME STREQUAL "Android")
|
||||||
|
|
||||||
|
else()
|
||||||
|
message("Unknown target platform: ${CMAKE_SYSTEM_NAME}")
|
||||||
|
message(FATAL_ERROR "Support your platform here")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
if (DEFINED ENABLE_PROFILING AND ENABLE_PROFILING)
|
||||||
|
if (CMAKE_COMPILER_IS_GNUCXX OR
|
||||||
|
CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg")
|
||||||
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pg")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -pg")
|
||||||
|
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -pg")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -pg")
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "Don't know how to enable profiling on your configuration")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
if (CMAKE_COMPILER_IS_GNUCXX)
|
||||||
|
# "When creating a static library using binutils (ar) and there
|
||||||
|
# exist a duplicate object name (e.g. a/Foo.cpp.o, b/Foo.cpp.o), the
|
||||||
|
# resulting static library can end up having only one of the
|
||||||
|
# duplicate objects. [...] This bug only happens if there are many
|
||||||
|
# objects." The trick consists in replacing the "r" argument
|
||||||
|
# ("replace") provided to "ar" (as used in CMake < 3.1) by the "q"
|
||||||
|
# argument ("quick append"). This is because of the fact that CMake
|
||||||
|
# will invoke "ar" several times with several batches of ".o"
|
||||||
|
# objects, and using "r" would overwrite symbols defined in
|
||||||
|
# preceding batches. https://cmake.org/Bug/view.php?id=14874
|
||||||
|
set(CMAKE_CXX_ARCHIVE_APPEND "<CMAKE_AR> <LINK_FLAGS> q <TARGET> <OBJECTS>")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
# This function defines macro "__ORTHANC_FILE__" as a replacement to
|
||||||
|
# macro "__FILE__", as the latter leaks the full path of the source
|
||||||
|
# files in the binaries
|
||||||
|
# https://stackoverflow.com/questions/8487986/file-macro-shows-full-path
|
||||||
|
# https://twitter.com/wget42/status/1676877802375634944?s=20
|
||||||
|
function(DefineSourceBasenameForTarget targetname)
|
||||||
|
# Microsoft Visual Studio is extremely slow if using
|
||||||
|
# "set_property()", we only enable this feature for gcc and clang
|
||||||
|
if (CMAKE_COMPILER_IS_GNUCXX OR
|
||||||
|
CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||||
|
get_target_property(source_files "${targetname}" SOURCES)
|
||||||
|
foreach(sourcefile ${source_files})
|
||||||
|
get_filename_component(basename "${sourcefile}" NAME)
|
||||||
|
set_property(
|
||||||
|
SOURCE "${sourcefile}" APPEND
|
||||||
|
PROPERTY COMPILE_DEFINITIONS "__ORTHANC_FILE__=\"${basename}\"")
|
||||||
|
endforeach()
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
578
Resources/Orthanc/CMake/DownloadOrthancFramework.cmake
Normal file
578
Resources/Orthanc/CMake/DownloadOrthancFramework.cmake
Normal file
@ -0,0 +1,578 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Check whether the parent script sets the mandatory variables
|
||||||
|
##
|
||||||
|
|
||||||
|
if (NOT DEFINED ORTHANC_FRAMEWORK_SOURCE OR
|
||||||
|
(NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "system" AND
|
||||||
|
NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" AND
|
||||||
|
NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "web" AND
|
||||||
|
NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" AND
|
||||||
|
NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "path"))
|
||||||
|
message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_SOURCE must be set to \"system\", \"hg\", \"web\", \"archive\" or \"path\"")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Detection of the requested version
|
||||||
|
##
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR
|
||||||
|
ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
|
||||||
|
ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
|
||||||
|
if (NOT DEFINED ORTHANC_FRAMEWORK_VERSION)
|
||||||
|
message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_VERSION must be set")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (DEFINED ORTHANC_FRAMEWORK_MAJOR OR
|
||||||
|
DEFINED ORTHANC_FRAMEWORK_MINOR OR
|
||||||
|
DEFINED ORTHANC_FRAMEWORK_REVISION OR
|
||||||
|
DEFINED ORTHANC_FRAMEWORK_MD5)
|
||||||
|
message(FATAL_ERROR "Some internal variable has been set")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "")
|
||||||
|
|
||||||
|
if (NOT DEFINED ORTHANC_FRAMEWORK_BRANCH)
|
||||||
|
if (ORTHANC_FRAMEWORK_VERSION STREQUAL "mainline")
|
||||||
|
set(ORTHANC_FRAMEWORK_BRANCH "default")
|
||||||
|
set(ORTHANC_FRAMEWORK_MAJOR 999)
|
||||||
|
set(ORTHANC_FRAMEWORK_MINOR 999)
|
||||||
|
set(ORTHANC_FRAMEWORK_REVISION 999)
|
||||||
|
|
||||||
|
else()
|
||||||
|
set(ORTHANC_FRAMEWORK_BRANCH "Orthanc-${ORTHANC_FRAMEWORK_VERSION}")
|
||||||
|
|
||||||
|
set(RE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
|
||||||
|
string(REGEX REPLACE ${RE} "\\1" ORTHANC_FRAMEWORK_MAJOR ${ORTHANC_FRAMEWORK_VERSION})
|
||||||
|
string(REGEX REPLACE ${RE} "\\2" ORTHANC_FRAMEWORK_MINOR ${ORTHANC_FRAMEWORK_VERSION})
|
||||||
|
string(REGEX REPLACE ${RE} "\\3" ORTHANC_FRAMEWORK_REVISION ${ORTHANC_FRAMEWORK_VERSION})
|
||||||
|
|
||||||
|
if (NOT ORTHANC_FRAMEWORK_MAJOR MATCHES "^[0-9]+$" OR
|
||||||
|
NOT ORTHANC_FRAMEWORK_MINOR MATCHES "^[0-9]+$" OR
|
||||||
|
NOT ORTHANC_FRAMEWORK_REVISION MATCHES "^[0-9]+$")
|
||||||
|
message("Bad version of the Orthanc framework, assuming a pre-release: ${ORTHANC_FRAMEWORK_VERSION}")
|
||||||
|
set(ORTHANC_FRAMEWORK_MAJOR 999)
|
||||||
|
set(ORTHANC_FRAMEWORK_MINOR 999)
|
||||||
|
set(ORTHANC_FRAMEWORK_REVISION 999)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.1")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "dac95bd6cf86fb19deaf4e612961f378")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.3.2")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "d0ccdf68e855d8224331f13774992750")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.0")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "81e15f34d97ac32bbd7d26e85698835a")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.1")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "9b6f6114264b17ed421b574cd6476127")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.4.2")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "d1ee84927dcf668e60eb5868d24b9394")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.0")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "4429d8d9dea4ff6648df80ec3c64d79e")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.1")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "099671538865e5da96208b37494d6718")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.2")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "8867050f3e9a1ce6157c1ea7a9433b1b")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.3")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "bf2f5ed1adb8b0fc5f10d278e68e1dfe")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.4")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "404baef5d4c43e7c5d9410edda8ef5a5")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.5")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "cfc437e0687ae4bd725fd93dc1f08bc4")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.6")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "3c29de1e289b5472342947168f0105c0")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.7")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "e1b76f01116d9b5d4ac8cc39980560e3")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.8")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "82323e8c49a667f658a3639ea4dbc336")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.0")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "eab428d6e53f61e847fa360bb17ebe25")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.6.1")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "3971f5de96ba71dc9d3f3690afeaa7c0")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.0")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "ce5f689e852b01d3672bd3d2f952a5ef")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.1")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "3c171217f930abe80246997bdbcaf7cc")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.2")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "328f94dcbd78c169655a13f7ad58a2c2")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.3")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "3f1ba9502ec7c5449971d3b56087bcde")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.7.4")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "19fcb7c21876af86546baa048a22c6c0")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.8.0")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "f8ec7554ef5d23ea4ce474b1e8214de9")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.8.1")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "db094f96399cbe8b9bbdbce34884c220")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.8.2")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "8bfa10e66c9931e74111be0bfb1f4548")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.0")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "cea0b02ce184671eaf1bd668beefbf28")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.1")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "08eebc66ef93c3b40115c38501db5fbd")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.2")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "3ea66c09f64aca990016683b6375734e")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.3")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "9b86e6f00e03278293cd15643cc0233f")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.4")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "6d5ca4a73ac7d42445041ca79de1624d")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.5")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "10fc64de1254a095e5d3ed3931f0cfbb")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.6")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "4b5d05683d747c29b2860ad79d11e62e")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.9.7")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "c912bbb860d640d3ae3003b5c9698205")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.10.0")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "8610c82d9153f22e929f2110f8f60279")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.10.1")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "caf667fc5ea452b3d0c2f70bfd02599c")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.0")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "962c4a4a706a2ef28b390d8515dd7091")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.1")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "a39661c406adf22cf574fde290cf4bbf")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.2")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "ede3de356493a8868545f8cb4b8bc8b5")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.11.3")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "e48fc0cb09c4856803791a1be28c07dc")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.0")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "d32a0cde03b6eb603d8dd2b33d38bf1b")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.1")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "8a435140efc8ff4a01d8242f092f21de")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.2")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "d2476b9e796e339ac320b5333489bdb3")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.3")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "975f5bf2142c22cb1777b4f6a0a614c5")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.12.4")
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "1e61779ea4a7cd705720bdcfed8a6a73")
|
||||||
|
|
||||||
|
# Below this point are development snapshots that were used to
|
||||||
|
# release some plugin, before an official release of the Orthanc
|
||||||
|
# framework was available. Here is the command to be used to
|
||||||
|
# generate a proper archive:
|
||||||
|
#
|
||||||
|
# $ hg archive /tmp/Orthanc-`hg id -i | sed 's/\+//'`.tar.gz
|
||||||
|
#
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "ae0e3fd609df")
|
||||||
|
# DICOMweb 1.1 (framework pre-1.6.0)
|
||||||
|
set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "7e09e9b530a2f527854f0b782d7e0645")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "82652c5fc04f")
|
||||||
|
# Stone Web viewer 1.0 (framework pre-1.8.1)
|
||||||
|
set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "d77331d68917e66a3f4f9b807bbdab7f")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "4a3ba4bf4ba7")
|
||||||
|
# PostgreSQL 3.3 (framework pre-1.8.2)
|
||||||
|
set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "2d82bddf06f9cfe82095495cb3b8abde")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "23ad1b9c7800")
|
||||||
|
# For "Toolbox::ReadJson()" and "Toolbox::Write{...}Json()" (pre-1.9.0)
|
||||||
|
set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "9af92080e57c60dd288eba46ce606c00")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "b2e08d83e21d")
|
||||||
|
# WSI 1.1 (framework pre-1.10.0), to remove "-std=c++11"
|
||||||
|
set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "2eaa073cbb4b44ffba199ad93393b2b1")
|
||||||
|
elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "daf4807631c5")
|
||||||
|
# DICOMweb 1.15 (framework pre-1.12.2)
|
||||||
|
set(ORTHANC_FRAMEWORK_PRE_RELEASE ON)
|
||||||
|
set(ORTHANC_FRAMEWORK_MD5 "ebe8bdf388319f1c9536b2b680451848")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
elseif (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path")
|
||||||
|
message("Using the Orthanc framework from a path of the filesystem. Assuming mainline version.")
|
||||||
|
set(ORTHANC_FRAMEWORK_MAJOR 999)
|
||||||
|
set(ORTHANC_FRAMEWORK_MINOR 999)
|
||||||
|
set(ORTHANC_FRAMEWORK_REVISION 999)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Detection of the third-party software
|
||||||
|
##
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg")
|
||||||
|
find_program(ORTHANC_FRAMEWORK_HG hg)
|
||||||
|
|
||||||
|
if (${ORTHANC_FRAMEWORK_HG} MATCHES "ORTHANC_FRAMEWORK_HG-NOTFOUND")
|
||||||
|
message(FATAL_ERROR "Please install Mercurial")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
|
||||||
|
ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
|
||||||
|
if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
|
||||||
|
find_program(ORTHANC_FRAMEWORK_7ZIP 7z
|
||||||
|
PATHS
|
||||||
|
"$ENV{ProgramFiles}/7-Zip"
|
||||||
|
"$ENV{ProgramW6432}/7-Zip"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (${ORTHANC_FRAMEWORK_7ZIP} MATCHES "ORTHANC_FRAMEWORK_7ZIP-NOTFOUND")
|
||||||
|
message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
else()
|
||||||
|
find_program(ORTHANC_FRAMEWORK_TAR tar)
|
||||||
|
if (${ORTHANC_FRAMEWORK_TAR} MATCHES "ORTHANC_FRAMEWORK_TAR-NOTFOUND")
|
||||||
|
message(FATAL_ERROR "Please install the 'tar' package")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Case of the Orthanc framework specified as a path on the filesystem
|
||||||
|
##
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "path")
|
||||||
|
if (NOT DEFINED ORTHANC_FRAMEWORK_ROOT OR
|
||||||
|
ORTHANC_FRAMEWORK_ROOT STREQUAL "")
|
||||||
|
message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ROOT must provide the path to the sources of Orthanc")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT})
|
||||||
|
message(FATAL_ERROR "Non-existing directory: ${ORTHANC_FRAMEWORK_ROOT}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Case of the Orthanc framework cloned using Mercurial
|
||||||
|
##
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg")
|
||||||
|
if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS)
|
||||||
|
message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(ORTHANC_ROOT ${CMAKE_BINARY_DIR}/orthanc)
|
||||||
|
|
||||||
|
if (EXISTS ${ORTHANC_ROOT})
|
||||||
|
message("Updating the Orthanc source repository using Mercurial")
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${ORTHANC_FRAMEWORK_HG} pull
|
||||||
|
WORKING_DIRECTORY ${ORTHANC_ROOT}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message("Forking the Orthanc source repository using Mercurial")
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${ORTHANC_FRAMEWORK_HG} clone "https://orthanc.uclouvain.be/hg/orthanc/"
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (Failure OR NOT EXISTS ${ORTHANC_ROOT})
|
||||||
|
message(FATAL_ERROR "Cannot fork the Orthanc repository")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message("Setting branch of the Orthanc repository to: ${ORTHANC_FRAMEWORK_BRANCH}")
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${ORTHANC_FRAMEWORK_HG} update -c ${ORTHANC_FRAMEWORK_BRANCH}
|
||||||
|
WORKING_DIRECTORY ${ORTHANC_ROOT}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Failure)
|
||||||
|
message(FATAL_ERROR "Error while running Mercurial")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Case of the Orthanc framework provided as a source archive on the
|
||||||
|
## filesystem
|
||||||
|
##
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive")
|
||||||
|
if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE OR
|
||||||
|
ORTHANC_FRAMEWORK_ARCHIVE STREQUAL "")
|
||||||
|
message(FATAL_ERROR "The variable ORTHANC_FRAMEWORK_ARCHIVE must provide the path to the sources of Orthanc")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Case of the Orthanc framework downloaded from the Web
|
||||||
|
##
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
|
||||||
|
if (DEFINED ORTHANC_FRAMEWORK_URL)
|
||||||
|
string(REGEX REPLACE "^.*/" "" ORTHANC_FRAMEMORK_FILENAME "${ORTHANC_FRAMEWORK_URL}")
|
||||||
|
else()
|
||||||
|
# Default case: Download from the official Web site
|
||||||
|
set(ORTHANC_FRAMEMORK_FILENAME Orthanc-${ORTHANC_FRAMEWORK_VERSION}.tar.gz)
|
||||||
|
if (ORTHANC_FRAMEWORK_PRE_RELEASE)
|
||||||
|
set(ORTHANC_FRAMEWORK_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/orthanc-framework/${ORTHANC_FRAMEMORK_FILENAME}")
|
||||||
|
else()
|
||||||
|
set(ORTHANC_FRAMEWORK_URL "https://orthanc.uclouvain.be/downloads/sources/orthanc/${ORTHANC_FRAMEMORK_FILENAME}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(ORTHANC_FRAMEWORK_ARCHIVE "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${ORTHANC_FRAMEMORK_FILENAME}")
|
||||||
|
|
||||||
|
if (NOT EXISTS "${ORTHANC_FRAMEWORK_ARCHIVE}")
|
||||||
|
if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS)
|
||||||
|
message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message("Downloading: ${ORTHANC_FRAMEWORK_URL}")
|
||||||
|
|
||||||
|
file(DOWNLOAD
|
||||||
|
"${ORTHANC_FRAMEWORK_URL}" "${ORTHANC_FRAMEWORK_ARCHIVE}"
|
||||||
|
SHOW_PROGRESS EXPECTED_MD5 "${ORTHANC_FRAMEWORK_MD5}"
|
||||||
|
TIMEOUT 60
|
||||||
|
INACTIVITY_TIMEOUT 60
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message("Using local copy of: ${ORTHANC_FRAMEWORK_URL}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Uncompressing the Orthanc framework, if it was retrieved from a
|
||||||
|
## source archive on the filesystem, or from the official Web site
|
||||||
|
##
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
|
||||||
|
ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
|
||||||
|
|
||||||
|
if (NOT DEFINED ORTHANC_FRAMEWORK_ARCHIVE OR
|
||||||
|
NOT DEFINED ORTHANC_FRAMEWORK_VERSION OR
|
||||||
|
NOT DEFINED ORTHANC_FRAMEWORK_MD5)
|
||||||
|
message(FATAL_ERROR "Internal error")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_MD5 STREQUAL "")
|
||||||
|
message(FATAL_ERROR "Unknown release of Orthanc: ${ORTHANC_FRAMEWORK_VERSION}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
file(MD5 ${ORTHANC_FRAMEWORK_ARCHIVE} ActualMD5)
|
||||||
|
|
||||||
|
if (NOT "${ActualMD5}" STREQUAL "${ORTHANC_FRAMEWORK_MD5}")
|
||||||
|
message(FATAL_ERROR "The MD5 hash of the Orthanc archive is invalid: ${ORTHANC_FRAMEWORK_ARCHIVE}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(ORTHANC_ROOT "${CMAKE_BINARY_DIR}/Orthanc-${ORTHANC_FRAMEWORK_VERSION}")
|
||||||
|
|
||||||
|
if (NOT IS_DIRECTORY "${ORTHANC_ROOT}")
|
||||||
|
if (NOT ORTHANC_FRAMEWORK_ARCHIVE MATCHES ".tar.gz$")
|
||||||
|
message(FATAL_ERROR "Archive should have the \".tar.gz\" extension: ${ORTHANC_FRAMEWORK_ARCHIVE}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message("Uncompressing: ${ORTHANC_FRAMEWORK_ARCHIVE}")
|
||||||
|
|
||||||
|
if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
|
||||||
|
# How to silently extract files using 7-zip
|
||||||
|
# http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${ORTHANC_FRAMEWORK_7ZIP} e -y ${ORTHANC_FRAMEWORK_ARCHIVE}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
OUTPUT_QUIET
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Failure)
|
||||||
|
message(FATAL_ERROR "Error while running the uncompression tool")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
get_filename_component(TMP_FILENAME "${ORTHANC_FRAMEWORK_ARCHIVE}" NAME)
|
||||||
|
string(REGEX REPLACE ".gz$" "" TMP_FILENAME2 "${TMP_FILENAME}")
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${ORTHANC_FRAMEWORK_7ZIP} x -y ${TMP_FILENAME2}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
OUTPUT_QUIET
|
||||||
|
)
|
||||||
|
|
||||||
|
else()
|
||||||
|
execute_process(
|
||||||
|
COMMAND sh -c "${ORTHANC_FRAMEWORK_TAR} xfz ${ORTHANC_FRAMEWORK_ARCHIVE}"
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (Failure)
|
||||||
|
message(FATAL_ERROR "Error while running the uncompression tool")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT IS_DIRECTORY "${ORTHANC_ROOT}")
|
||||||
|
message(FATAL_ERROR "The Orthanc framework was not uncompressed at the proper location. Check the CMake instructions.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Determine the path to the sources of the Orthanc framework
|
||||||
|
##
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "archive" OR
|
||||||
|
ORTHANC_FRAMEWORK_SOURCE STREQUAL "hg" OR
|
||||||
|
ORTHANC_FRAMEWORK_SOURCE STREQUAL "web")
|
||||||
|
if (NOT DEFINED ORTHANC_ROOT OR
|
||||||
|
NOT DEFINED ORTHANC_FRAMEWORK_MAJOR OR
|
||||||
|
NOT DEFINED ORTHANC_FRAMEWORK_MINOR OR
|
||||||
|
NOT DEFINED ORTHANC_FRAMEWORK_REVISION)
|
||||||
|
message(FATAL_ERROR "Internal error in the DownloadOrthancFramework.cmake file")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
unset(ORTHANC_FRAMEWORK_ROOT CACHE)
|
||||||
|
|
||||||
|
if ("${ORTHANC_FRAMEWORK_MAJOR}.${ORTHANC_FRAMEWORK_MINOR}.${ORTHANC_FRAMEWORK_REVISION}" VERSION_LESS "1.7.2")
|
||||||
|
set(ORTHANC_FRAMEWORK_ROOT "${ORTHANC_ROOT}/Core" CACHE
|
||||||
|
STRING "Path to the Orthanc framework source directory")
|
||||||
|
set(ENABLE_PLUGINS_VERSION_SCRIPT OFF)
|
||||||
|
else()
|
||||||
|
set(ORTHANC_FRAMEWORK_ROOT "${ORTHANC_ROOT}/OrthancFramework/Sources" CACHE
|
||||||
|
STRING "Path to the Orthanc framework source directory")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
unset(ORTHANC_ROOT)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT ORTHANC_FRAMEWORK_SOURCE STREQUAL "system")
|
||||||
|
if (NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT}/OrthancException.h OR
|
||||||
|
NOT EXISTS ${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkParameters.cmake)
|
||||||
|
message(FATAL_ERROR "Directory not containing the source code of the Orthanc framework: ${ORTHANC_FRAMEWORK_ROOT}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Case of the Orthanc framework installed as a shared library in a
|
||||||
|
## GNU/Linux distribution (typically Debian). New in Orthanc 1.7.2.
|
||||||
|
##
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_SOURCE STREQUAL "system")
|
||||||
|
set(ORTHANC_FRAMEWORK_LIBDIR "" CACHE PATH "")
|
||||||
|
set(ORTHANC_FRAMEWORK_USE_SHARED ON CACHE BOOL "Whether to use the shared library or the static library")
|
||||||
|
set(ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES "" CACHE STRING "Additional libraries to link against, separated by whitespaces, typically needed if using the static library (a common minimal value is \"boost_filesystem boost_iostreams boost_locale boost_regex boost_thread jsoncpp pugixml uuid\")")
|
||||||
|
|
||||||
|
if (CMAKE_SYSTEM_NAME STREQUAL "Windows" AND
|
||||||
|
CMAKE_COMPILER_IS_GNUCXX) # MinGW
|
||||||
|
set(DYNAMIC_MINGW_STDLIB ON) # Disable static linking against libc (to throw exceptions)
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -static-libstdc++")
|
||||||
|
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include(CheckIncludeFile)
|
||||||
|
include(CheckIncludeFileCXX)
|
||||||
|
include(FindPythonInterp)
|
||||||
|
include(${CMAKE_CURRENT_LIST_DIR}/Compiler.cmake)
|
||||||
|
include(${CMAKE_CURRENT_LIST_DIR}/DownloadPackage.cmake)
|
||||||
|
include(${CMAKE_CURRENT_LIST_DIR}/AutoGeneratedCode.cmake)
|
||||||
|
set(EMBED_RESOURCES_PYTHON ${CMAKE_CURRENT_LIST_DIR}/EmbedResources.py)
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_USE_SHARED)
|
||||||
|
list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix)
|
||||||
|
list(GET CMAKE_FIND_LIBRARY_SUFFIXES 0 Suffix)
|
||||||
|
else()
|
||||||
|
list(GET CMAKE_FIND_LIBRARY_PREFIXES 0 Prefix)
|
||||||
|
list(GET CMAKE_FIND_LIBRARY_SUFFIXES 1 Suffix)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# The "OrthancFramework" library must be the first one to be included
|
||||||
|
if ("${ORTHANC_FRAMEWORK_LIBDIR}" STREQUAL "")
|
||||||
|
set(ORTHANC_FRAMEWORK_LIBRARIES ${Prefix}OrthancFramework${Suffix})
|
||||||
|
else ()
|
||||||
|
set(ORTHANC_FRAMEWORK_LIBRARIES ${ORTHANC_FRAMEWORK_LIBDIR}/${Prefix}OrthancFramework${Suffix})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES STREQUAL "")
|
||||||
|
# https://stackoverflow.com/a/5272993/881731
|
||||||
|
string(REPLACE " " ";" tmp ${ORTHANC_FRAMEWORK_ADDITIONAL_LIBRARIES})
|
||||||
|
list(APPEND ORTHANC_FRAMEWORK_LIBRARIES ${tmp})
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Look for the version of the mandatory dependency JsonCpp (cf. JsonCppConfiguration.cmake)
|
||||||
|
if (CMAKE_CROSSCOMPILING)
|
||||||
|
set(JSONCPP_INCLUDE_DIR ${ORTHANC_FRAMEWORK_ROOT}/..)
|
||||||
|
else()
|
||||||
|
find_path(JSONCPP_INCLUDE_DIR json/reader.h
|
||||||
|
${ORTHANC_FRAMEWORK_ROOT}/..
|
||||||
|
/usr/include/jsoncpp
|
||||||
|
/usr/local/include/jsoncpp
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message("JsonCpp include dir: ${JSONCPP_INCLUDE_DIR}")
|
||||||
|
include_directories(${JSONCPP_INCLUDE_DIR})
|
||||||
|
|
||||||
|
CHECK_INCLUDE_FILE_CXX(${JSONCPP_INCLUDE_DIR}/json/reader.h HAVE_JSONCPP_H)
|
||||||
|
if (NOT HAVE_JSONCPP_H)
|
||||||
|
message(FATAL_ERROR "Please install the libjsoncpp-dev package")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Look for Orthanc framework shared library
|
||||||
|
include(CheckCXXSymbolExists)
|
||||||
|
|
||||||
|
if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
|
||||||
|
set(ORTHANC_FRAMEWORK_INCLUDE_DIR ${ORTHANC_FRAMEWORK_ROOT})
|
||||||
|
else()
|
||||||
|
find_path(ORTHANC_FRAMEWORK_INCLUDE_DIR OrthancFramework.h
|
||||||
|
/usr/include/orthanc-framework
|
||||||
|
/usr/local/include/orthanc-framework
|
||||||
|
${ORTHANC_FRAMEWORK_ROOT}
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (${ORTHANC_FRAMEWORK_INCLUDE_DIR} STREQUAL "ORTHANC_FRAMEWORK_INCLUDE_DIR-NOTFOUND")
|
||||||
|
message(FATAL_ERROR "Cannot locate the OrthancFramework.h header")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
message("Orthanc framework include dir: ${ORTHANC_FRAMEWORK_INCLUDE_DIR}")
|
||||||
|
include_directories(${ORTHANC_FRAMEWORK_INCLUDE_DIR})
|
||||||
|
|
||||||
|
if (ORTHANC_FRAMEWORK_USE_SHARED)
|
||||||
|
set(CMAKE_REQUIRED_INCLUDES "${ORTHANC_FRAMEWORK_INCLUDE_DIR}")
|
||||||
|
set(CMAKE_REQUIRED_LIBRARIES "${ORTHANC_FRAMEWORK_LIBRARIES}")
|
||||||
|
|
||||||
|
check_cxx_symbol_exists("Orthanc::InitializeFramework" "OrthancFramework.h" HAVE_ORTHANC_FRAMEWORK)
|
||||||
|
if (NOT HAVE_ORTHANC_FRAMEWORK)
|
||||||
|
message(FATAL_ERROR "Cannot find the Orthanc framework")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
unset(CMAKE_REQUIRED_INCLUDES)
|
||||||
|
unset(CMAKE_REQUIRED_LIBRARIES)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
287
Resources/Orthanc/CMake/DownloadPackage.cmake
Normal file
287
Resources/Orthanc/CMake/DownloadPackage.cmake
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
macro(GetUrlFilename TargetVariable Url)
|
||||||
|
string(REGEX REPLACE "^.*/" "" ${TargetVariable} "${Url}")
|
||||||
|
endmacro()
|
||||||
|
|
||||||
|
|
||||||
|
macro(GetUrlExtension TargetVariable Url)
|
||||||
|
#string(REGEX REPLACE "^.*/[^.]*\\." "" TMP "${Url}")
|
||||||
|
string(REGEX REPLACE "^.*\\." "" TMP "${Url}")
|
||||||
|
string(TOLOWER "${TMP}" "${TargetVariable}")
|
||||||
|
endmacro()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Setup the patch command-line tool
|
||||||
|
##
|
||||||
|
|
||||||
|
if (NOT ORTHANC_DISABLE_PATCH)
|
||||||
|
if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
|
||||||
|
set(PATCH_EXECUTABLE ${CMAKE_CURRENT_LIST_DIR}/../ThirdParty/patch/patch.exe)
|
||||||
|
if (NOT EXISTS ${PATCH_EXECUTABLE})
|
||||||
|
message(FATAL_ERROR "Unable to find the patch.exe tool that is shipped with Orthanc")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
else ()
|
||||||
|
find_program(PATCH_EXECUTABLE patch)
|
||||||
|
if (${PATCH_EXECUTABLE} MATCHES "PATCH_EXECUTABLE-NOTFOUND")
|
||||||
|
message(FATAL_ERROR "Please install the 'patch' standard command-line tool")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## Check the existence of the required decompression tools
|
||||||
|
##
|
||||||
|
|
||||||
|
if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
|
||||||
|
find_program(ZIP_EXECUTABLE 7z
|
||||||
|
PATHS
|
||||||
|
"$ENV{ProgramFiles}/7-Zip"
|
||||||
|
"$ENV{ProgramW6432}/7-Zip"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (${ZIP_EXECUTABLE} MATCHES "ZIP_EXECUTABLE-NOTFOUND")
|
||||||
|
message(FATAL_ERROR "Please install the '7-zip' software (http://www.7-zip.org/)")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
else()
|
||||||
|
find_program(UNZIP_EXECUTABLE unzip)
|
||||||
|
if (${UNZIP_EXECUTABLE} MATCHES "UNZIP_EXECUTABLE-NOTFOUND")
|
||||||
|
message(FATAL_ERROR "Please install the 'unzip' package")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_program(TAR_EXECUTABLE tar)
|
||||||
|
if (${TAR_EXECUTABLE} MATCHES "TAR_EXECUTABLE-NOTFOUND")
|
||||||
|
message(FATAL_ERROR "Please install the 'tar' package")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_program(GUNZIP_EXECUTABLE gunzip)
|
||||||
|
if (${GUNZIP_EXECUTABLE} MATCHES "GUNZIP_EXECUTABLE-NOTFOUND")
|
||||||
|
message(FATAL_ERROR "Please install the 'gzip' package")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
|
macro(DownloadFile MD5 Url)
|
||||||
|
GetUrlFilename(TMP_FILENAME "${Url}")
|
||||||
|
|
||||||
|
set(TMP_PATH "${CMAKE_SOURCE_DIR}/ThirdPartyDownloads/${TMP_FILENAME}")
|
||||||
|
if (NOT EXISTS "${TMP_PATH}")
|
||||||
|
message("Downloading ${Url}")
|
||||||
|
|
||||||
|
# This fixes issue 6: "I think cmake shouldn't download the
|
||||||
|
# packages which are not in the system, it should stop and let
|
||||||
|
# user know."
|
||||||
|
# https://code.google.com/p/orthanc/issues/detail?id=6
|
||||||
|
if (NOT STATIC_BUILD AND NOT ALLOW_DOWNLOADS)
|
||||||
|
message(FATAL_ERROR "CMake is not allowed to download from Internet. Please set the ALLOW_DOWNLOADS option to ON")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
foreach (retry RANGE 1 5) # Retries 5 times
|
||||||
|
if ("${MD5}" STREQUAL "no-check")
|
||||||
|
message(WARNING "Not checking the MD5 of: ${Url}")
|
||||||
|
file(DOWNLOAD "${Url}" "${TMP_PATH}"
|
||||||
|
SHOW_PROGRESS TIMEOUT 30 INACTIVITY_TIMEOUT 10
|
||||||
|
STATUS Failure)
|
||||||
|
else()
|
||||||
|
file(DOWNLOAD "${Url}" "${TMP_PATH}"
|
||||||
|
SHOW_PROGRESS TIMEOUT 30 INACTIVITY_TIMEOUT 10
|
||||||
|
EXPECTED_MD5 "${MD5}" STATUS Failure)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
list(GET Failure 0 Status)
|
||||||
|
if (Status EQUAL 0)
|
||||||
|
break() # Successful download
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
if (NOT Status EQUAL 0)
|
||||||
|
file(REMOVE ${TMP_PATH})
|
||||||
|
message(FATAL_ERROR "Cannot download file: ${Url}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
else()
|
||||||
|
message("Using local copy of ${Url}")
|
||||||
|
|
||||||
|
if ("${MD5}" STREQUAL "no-check")
|
||||||
|
message(WARNING "Not checking the MD5 of: ${Url}")
|
||||||
|
else()
|
||||||
|
file(MD5 ${TMP_PATH} ActualMD5)
|
||||||
|
if (NOT "${ActualMD5}" STREQUAL "${MD5}")
|
||||||
|
message(FATAL_ERROR "The MD5 hash of a previously download file is invalid: ${TMP_PATH}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endmacro()
|
||||||
|
|
||||||
|
|
||||||
|
macro(DownloadPackage MD5 Url TargetDirectory)
|
||||||
|
if (NOT IS_DIRECTORY "${TargetDirectory}")
|
||||||
|
DownloadFile("${MD5}" "${Url}")
|
||||||
|
|
||||||
|
GetUrlExtension(TMP_EXTENSION "${Url}")
|
||||||
|
#message(${TMP_EXTENSION})
|
||||||
|
message("Uncompressing ${TMP_FILENAME}")
|
||||||
|
|
||||||
|
if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
|
||||||
|
# How to silently extract files using 7-zip
|
||||||
|
# http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly
|
||||||
|
|
||||||
|
if (("${TMP_EXTENSION}" STREQUAL "gz") OR
|
||||||
|
("${TMP_EXTENSION}" STREQUAL "tgz") OR
|
||||||
|
("${TMP_EXTENSION}" STREQUAL "xz"))
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${ZIP_EXECUTABLE} e -y ${TMP_PATH}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
OUTPUT_QUIET
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Failure)
|
||||||
|
message(FATAL_ERROR "Error while running the uncompression tool")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if ("${TMP_EXTENSION}" STREQUAL "tgz")
|
||||||
|
string(REGEX REPLACE ".tgz$" ".tar" TMP_FILENAME2 "${TMP_FILENAME}")
|
||||||
|
elseif ("${TMP_EXTENSION}" STREQUAL "gz")
|
||||||
|
string(REGEX REPLACE ".gz$" "" TMP_FILENAME2 "${TMP_FILENAME}")
|
||||||
|
elseif ("${TMP_EXTENSION}" STREQUAL "xz")
|
||||||
|
string(REGEX REPLACE ".xz" "" TMP_FILENAME2 "${TMP_FILENAME}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${ZIP_EXECUTABLE} x -y ${TMP_FILENAME2}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
OUTPUT_QUIET
|
||||||
|
)
|
||||||
|
elseif ("${TMP_EXTENSION}" STREQUAL "zip")
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${ZIP_EXECUTABLE} x -y ${TMP_PATH}
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
OUTPUT_QUIET
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
else()
|
||||||
|
if ("${TMP_EXTENSION}" STREQUAL "zip")
|
||||||
|
execute_process(
|
||||||
|
COMMAND sh -c "${UNZIP_EXECUTABLE} -q ${TMP_PATH}"
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
)
|
||||||
|
elseif (("${TMP_EXTENSION}" STREQUAL "gz") OR ("${TMP_EXTENSION}" STREQUAL "tgz"))
|
||||||
|
#message("tar xvfz ${TMP_PATH}")
|
||||||
|
execute_process(
|
||||||
|
COMMAND sh -c "${TAR_EXECUTABLE} xfz ${TMP_PATH}"
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
)
|
||||||
|
elseif ("${TMP_EXTENSION}" STREQUAL "bz2")
|
||||||
|
execute_process(
|
||||||
|
COMMAND sh -c "${TAR_EXECUTABLE} xfj ${TMP_PATH}"
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
)
|
||||||
|
elseif ("${TMP_EXTENSION}" STREQUAL "xz")
|
||||||
|
execute_process(
|
||||||
|
COMMAND sh -c "${TAR_EXECUTABLE} xf ${TMP_PATH}"
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "Unsupported package extension: ${TMP_EXTENSION}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (Failure)
|
||||||
|
message(FATAL_ERROR "Error while running the uncompression tool")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT IS_DIRECTORY "${TargetDirectory}")
|
||||||
|
message(FATAL_ERROR "The package was not uncompressed at the proper location. Check the CMake instructions.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endmacro()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
macro(DownloadCompressedFile MD5 Url TargetFile)
|
||||||
|
if (NOT EXISTS "${TargetFile}")
|
||||||
|
DownloadFile("${MD5}" "${Url}")
|
||||||
|
|
||||||
|
GetUrlExtension(TMP_EXTENSION "${Url}")
|
||||||
|
#message(${TMP_EXTENSION})
|
||||||
|
message("Uncompressing ${TMP_FILENAME}")
|
||||||
|
|
||||||
|
if ("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
|
||||||
|
# How to silently extract files using 7-zip
|
||||||
|
# http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly
|
||||||
|
|
||||||
|
if ("${TMP_EXTENSION}" STREQUAL "gz")
|
||||||
|
execute_process(
|
||||||
|
# "-so" writes uncompressed file to stdout
|
||||||
|
COMMAND ${ZIP_EXECUTABLE} e -so -y ${TMP_PATH}
|
||||||
|
OUTPUT_FILE "${TargetFile}"
|
||||||
|
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
OUTPUT_QUIET
|
||||||
|
)
|
||||||
|
|
||||||
|
if (Failure)
|
||||||
|
message(FATAL_ERROR "Error while running the uncompression tool")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
else()
|
||||||
|
if ("${TMP_EXTENSION}" STREQUAL "gz")
|
||||||
|
execute_process(
|
||||||
|
COMMAND sh -c "${GUNZIP_EXECUTABLE} -c ${TMP_PATH}"
|
||||||
|
OUTPUT_FILE "${TargetFile}"
|
||||||
|
RESULT_VARIABLE Failure
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR "Unsupported file extension: ${TMP_EXTENSION}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (Failure)
|
||||||
|
message(FATAL_ERROR "Error while running the uncompression tool")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT EXISTS "${TargetFile}")
|
||||||
|
message(FATAL_ERROR "The file was not uncompressed at the proper location. Check the CMake instructions.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
endmacro()
|
||||||
446
Resources/Orthanc/CMake/EmbedResources.py
Normal file
446
Resources/Orthanc/CMake/EmbedResources.py
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
import pprint
|
||||||
|
import re
|
||||||
|
|
||||||
|
UPCASE_CHECK = True
|
||||||
|
USE_SYSTEM_EXCEPTION = False
|
||||||
|
EXCEPTION_CLASS = 'OrthancException'
|
||||||
|
OUT_OF_RANGE_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_ParameterOutOfRange)'
|
||||||
|
INEXISTENT_PATH_EXCEPTION = '::Orthanc::OrthancException(::Orthanc::ErrorCode_InexistentItem)'
|
||||||
|
NAMESPACE = 'Orthanc.EmbeddedResources'
|
||||||
|
FRAMEWORK_PATH = None
|
||||||
|
|
||||||
|
ARGS = []
|
||||||
|
for i in range(len(sys.argv)):
|
||||||
|
if not sys.argv[i].startswith('--'):
|
||||||
|
ARGS.append(sys.argv[i])
|
||||||
|
elif sys.argv[i].lower() == '--no-upcase-check':
|
||||||
|
UPCASE_CHECK = False
|
||||||
|
elif sys.argv[i].lower() == '--system-exception':
|
||||||
|
USE_SYSTEM_EXCEPTION = True
|
||||||
|
EXCEPTION_CLASS = '::std::runtime_error'
|
||||||
|
OUT_OF_RANGE_EXCEPTION = '%s("Parameter out of range")' % EXCEPTION_CLASS
|
||||||
|
INEXISTENT_PATH_EXCEPTION = '%s("Unknown path in a directory resource")' % EXCEPTION_CLASS
|
||||||
|
elif sys.argv[i].startswith('--namespace='):
|
||||||
|
NAMESPACE = sys.argv[i][sys.argv[i].find('=') + 1 : ]
|
||||||
|
elif sys.argv[i].startswith('--framework-path='):
|
||||||
|
FRAMEWORK_PATH = sys.argv[i][sys.argv[i].find('=') + 1 : ]
|
||||||
|
|
||||||
|
if len(ARGS) < 2 or len(ARGS) % 2 != 0:
|
||||||
|
print ('Usage:')
|
||||||
|
print ('python %s [--no-upcase-check] [--system-exception] [--namespace=<Namespace>] <TargetBaseFilename> [ <Name> <Source> ]*' % sys.argv[0])
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
TARGET_BASE_FILENAME = ARGS[1]
|
||||||
|
SOURCES = ARGS[2:]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Make sure the destination directory exists
|
||||||
|
os.makedirs(os.path.normpath(os.path.join(TARGET_BASE_FILENAME, '..')))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
## Read each resource file
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
def CheckNoUpcase(s):
|
||||||
|
global UPCASE_CHECK
|
||||||
|
if (UPCASE_CHECK and
|
||||||
|
re.search('[A-Z]', s) != None):
|
||||||
|
raise Exception("Path in a directory with an upcase letter: %s" % s)
|
||||||
|
|
||||||
|
resources = {}
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
i = 0
|
||||||
|
while i < len(SOURCES):
|
||||||
|
resourceName = SOURCES[i].upper()
|
||||||
|
pathName = SOURCES[i + 1]
|
||||||
|
|
||||||
|
if not os.path.exists(pathName):
|
||||||
|
raise Exception("Non existing path: %s" % pathName)
|
||||||
|
|
||||||
|
if resourceName in resources:
|
||||||
|
raise Exception("Twice the same resource: " + resourceName)
|
||||||
|
|
||||||
|
if os.path.isdir(pathName):
|
||||||
|
# The resource is a directory: Recursively explore its files
|
||||||
|
content = {}
|
||||||
|
for root, dirs, files in os.walk(pathName):
|
||||||
|
dirs.sort()
|
||||||
|
files.sort()
|
||||||
|
base = os.path.relpath(root, pathName)
|
||||||
|
|
||||||
|
# Fix issue #24 (Build fails on OSX when directory has .DS_Store files):
|
||||||
|
# Ignore folders whose name starts with a dot (".")
|
||||||
|
if base.find('/.') != -1:
|
||||||
|
print('Ignoring folder: %s' % root)
|
||||||
|
continue
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
if f.find('~') == -1: # Ignore Emacs backup files
|
||||||
|
if base == '.':
|
||||||
|
r = f
|
||||||
|
else:
|
||||||
|
r = os.path.join(base, f)
|
||||||
|
|
||||||
|
CheckNoUpcase(r)
|
||||||
|
r = '/' + r.replace('\\', '/')
|
||||||
|
if r in content:
|
||||||
|
raise Exception("Twice the same filename (check case): " + r)
|
||||||
|
|
||||||
|
content[r] = {
|
||||||
|
'Filename' : os.path.join(root, f),
|
||||||
|
'Index' : counter
|
||||||
|
}
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
resources[resourceName] = {
|
||||||
|
'Type' : 'Directory',
|
||||||
|
'Files' : content
|
||||||
|
}
|
||||||
|
|
||||||
|
elif os.path.isfile(pathName):
|
||||||
|
resources[resourceName] = {
|
||||||
|
'Type' : 'File',
|
||||||
|
'Index' : counter,
|
||||||
|
'Filename' : pathName
|
||||||
|
}
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("Not a regular file, nor a directory: " + pathName)
|
||||||
|
|
||||||
|
i += 2
|
||||||
|
|
||||||
|
#pprint.pprint(resources)
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
## Write .h header
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
header = open(TARGET_BASE_FILENAME + '.h', 'w')
|
||||||
|
|
||||||
|
header.write("""
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <list>
|
||||||
|
|
||||||
|
#if defined(_MSC_VER)
|
||||||
|
# pragma warning(disable: 4065) // "Switch statement contains 'default' but no 'case' labels"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
for ns in NAMESPACE.split('.'):
|
||||||
|
header.write('namespace %s {\n' % ns)
|
||||||
|
|
||||||
|
|
||||||
|
header.write("""
|
||||||
|
enum FileResourceId
|
||||||
|
{
|
||||||
|
""")
|
||||||
|
|
||||||
|
isFirst = True
|
||||||
|
for name in resources:
|
||||||
|
if resources[name]['Type'] == 'File':
|
||||||
|
if isFirst:
|
||||||
|
isFirst = False
|
||||||
|
else:
|
||||||
|
header.write(',\n')
|
||||||
|
header.write(' %s' % name)
|
||||||
|
|
||||||
|
header.write("""
|
||||||
|
};
|
||||||
|
|
||||||
|
enum DirectoryResourceId
|
||||||
|
{
|
||||||
|
""")
|
||||||
|
|
||||||
|
isFirst = True
|
||||||
|
for name in resources:
|
||||||
|
if resources[name]['Type'] == 'Directory':
|
||||||
|
if isFirst:
|
||||||
|
isFirst = False
|
||||||
|
else:
|
||||||
|
header.write(',\n')
|
||||||
|
header.write(' %s' % name)
|
||||||
|
|
||||||
|
header.write("""
|
||||||
|
};
|
||||||
|
|
||||||
|
const void* GetFileResourceBuffer(FileResourceId id);
|
||||||
|
size_t GetFileResourceSize(FileResourceId id);
|
||||||
|
void GetFileResource(std::string& result, FileResourceId id);
|
||||||
|
|
||||||
|
const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path);
|
||||||
|
size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path);
|
||||||
|
void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path);
|
||||||
|
|
||||||
|
void ListResources(std::list<std::string>& result, DirectoryResourceId id);
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
for ns in NAMESPACE.split('.'):
|
||||||
|
header.write('}\n')
|
||||||
|
|
||||||
|
header.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
## Write the resource content in the .cpp source
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
PYTHON_MAJOR_VERSION = sys.version_info[0]
|
||||||
|
|
||||||
|
def WriteResource(cpp, item):
|
||||||
|
cpp.write(' static const uint8_t resource%dBuffer[] = {' % item['Index'])
|
||||||
|
|
||||||
|
f = open(item['Filename'], "rb")
|
||||||
|
content = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
# http://stackoverflow.com/a/1035360
|
||||||
|
pos = 0
|
||||||
|
buffer = [] # instead of appending a few bytes at a time to the cpp file,
|
||||||
|
# we first append each chunk to a list, join it and write it
|
||||||
|
# to the file. We've measured that it was 2-3 times faster in python3.
|
||||||
|
# Note that speed is important since if generation is too slow,
|
||||||
|
# cmake might try to compile the EmbeddedResources.cpp file while it is
|
||||||
|
# still being generated !
|
||||||
|
for b in content:
|
||||||
|
if PYTHON_MAJOR_VERSION == 2:
|
||||||
|
c = ord(b[0])
|
||||||
|
else:
|
||||||
|
c = b
|
||||||
|
|
||||||
|
if pos > 0:
|
||||||
|
buffer.append(",")
|
||||||
|
|
||||||
|
if (pos % 16) == 0:
|
||||||
|
buffer.append("\n")
|
||||||
|
|
||||||
|
if c < 0:
|
||||||
|
raise Exception("Internal error")
|
||||||
|
|
||||||
|
buffer.append("0x%02x" % c)
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
cpp.write("".join(buffer))
|
||||||
|
# Zero-size array are disallowed, so we put one single void character in it.
|
||||||
|
if pos == 0:
|
||||||
|
cpp.write(' 0')
|
||||||
|
|
||||||
|
cpp.write(' };\n')
|
||||||
|
cpp.write(' static const size_t resource%dSize = %d;\n' % (item['Index'], pos))
|
||||||
|
|
||||||
|
|
||||||
|
cpp = open(TARGET_BASE_FILENAME + '.cpp', 'w')
|
||||||
|
|
||||||
|
cpp.write('#include "%s.h"\n' % os.path.basename(TARGET_BASE_FILENAME))
|
||||||
|
|
||||||
|
if USE_SYSTEM_EXCEPTION:
|
||||||
|
cpp.write('#include <stdexcept>')
|
||||||
|
elif FRAMEWORK_PATH != None:
|
||||||
|
cpp.write('#include "%s/OrthancException.h"' % FRAMEWORK_PATH)
|
||||||
|
else:
|
||||||
|
cpp.write('#include <OrthancException.h>')
|
||||||
|
|
||||||
|
cpp.write("""
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
for ns in NAMESPACE.split('.'):
|
||||||
|
cpp.write('namespace %s {\n' % ns)
|
||||||
|
|
||||||
|
|
||||||
|
for name in resources:
|
||||||
|
if resources[name]['Type'] == 'File':
|
||||||
|
WriteResource(cpp, resources[name])
|
||||||
|
else:
|
||||||
|
for f in resources[name]['Files']:
|
||||||
|
WriteResource(cpp, resources[name]['Files'][f])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
## Write the accessors to the file resources in .cpp
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
cpp.write("""
|
||||||
|
const void* GetFileResourceBuffer(FileResourceId id)
|
||||||
|
{
|
||||||
|
switch (id)
|
||||||
|
{
|
||||||
|
""")
|
||||||
|
for name in resources:
|
||||||
|
if resources[name]['Type'] == 'File':
|
||||||
|
cpp.write(' case %s:\n' % name)
|
||||||
|
cpp.write(' return resource%dBuffer;\n' % resources[name]['Index'])
|
||||||
|
|
||||||
|
cpp.write("""
|
||||||
|
default:
|
||||||
|
throw %s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t GetFileResourceSize(FileResourceId id)
|
||||||
|
{
|
||||||
|
switch (id)
|
||||||
|
{
|
||||||
|
""" % OUT_OF_RANGE_EXCEPTION)
|
||||||
|
|
||||||
|
for name in resources:
|
||||||
|
if resources[name]['Type'] == 'File':
|
||||||
|
cpp.write(' case %s:\n' % name)
|
||||||
|
cpp.write(' return resource%dSize;\n' % resources[name]['Index'])
|
||||||
|
|
||||||
|
cpp.write("""
|
||||||
|
default:
|
||||||
|
throw %s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""" % OUT_OF_RANGE_EXCEPTION)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
## Write the accessors to the directory resources in .cpp
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
cpp.write("""
|
||||||
|
const void* GetDirectoryResourceBuffer(DirectoryResourceId id, const char* path)
|
||||||
|
{
|
||||||
|
switch (id)
|
||||||
|
{
|
||||||
|
""")
|
||||||
|
|
||||||
|
for name in resources:
|
||||||
|
if resources[name]['Type'] == 'Directory':
|
||||||
|
cpp.write(' case %s:\n' % name)
|
||||||
|
isFirst = True
|
||||||
|
for path in resources[name]['Files']:
|
||||||
|
cpp.write(' if (!strcmp(path, "%s"))\n' % path)
|
||||||
|
cpp.write(' return resource%dBuffer;\n' % resources[name]['Files'][path]['Index'])
|
||||||
|
cpp.write(' throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION)
|
||||||
|
|
||||||
|
cpp.write(""" default:
|
||||||
|
throw %s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t GetDirectoryResourceSize(DirectoryResourceId id, const char* path)
|
||||||
|
{
|
||||||
|
switch (id)
|
||||||
|
{
|
||||||
|
""" % OUT_OF_RANGE_EXCEPTION)
|
||||||
|
|
||||||
|
for name in resources:
|
||||||
|
if resources[name]['Type'] == 'Directory':
|
||||||
|
cpp.write(' case %s:\n' % name)
|
||||||
|
isFirst = True
|
||||||
|
for path in resources[name]['Files']:
|
||||||
|
cpp.write(' if (!strcmp(path, "%s"))\n' % path)
|
||||||
|
cpp.write(' return resource%dSize;\n' % resources[name]['Files'][path]['Index'])
|
||||||
|
cpp.write(' throw %s;\n\n' % INEXISTENT_PATH_EXCEPTION)
|
||||||
|
|
||||||
|
cpp.write(""" default:
|
||||||
|
throw %s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""" % OUT_OF_RANGE_EXCEPTION)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
## List the resources in a directory
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
cpp.write("""
|
||||||
|
void ListResources(std::list<std::string>& result, DirectoryResourceId id)
|
||||||
|
{
|
||||||
|
result.clear();
|
||||||
|
|
||||||
|
switch (id)
|
||||||
|
{
|
||||||
|
""")
|
||||||
|
|
||||||
|
for name in resources:
|
||||||
|
if resources[name]['Type'] == 'Directory':
|
||||||
|
cpp.write(' case %s:\n' % name)
|
||||||
|
for path in sorted(resources[name]['Files']):
|
||||||
|
cpp.write(' result.push_back("%s");\n' % path)
|
||||||
|
cpp.write(' break;\n\n')
|
||||||
|
|
||||||
|
cpp.write(""" default:
|
||||||
|
throw %s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""" % OUT_OF_RANGE_EXCEPTION)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
## Write the convenience wrappers in .cpp
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
cpp.write("""
|
||||||
|
void GetFileResource(std::string& result, FileResourceId id)
|
||||||
|
{
|
||||||
|
size_t size = GetFileResourceSize(id);
|
||||||
|
result.resize(size);
|
||||||
|
if (size > 0)
|
||||||
|
memcpy(&result[0], GetFileResourceBuffer(id), size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GetDirectoryResource(std::string& result, DirectoryResourceId id, const char* path)
|
||||||
|
{
|
||||||
|
size_t size = GetDirectoryResourceSize(id, path);
|
||||||
|
result.resize(size);
|
||||||
|
if (size > 0)
|
||||||
|
memcpy(&result[0], GetDirectoryResourceBuffer(id, path), size);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
for ns in NAMESPACE.split('.'):
|
||||||
|
cpp.write('}\n')
|
||||||
|
|
||||||
|
cpp.close()
|
||||||
91
Resources/Orthanc/CMake/GoogleTestConfiguration.cmake
Normal file
91
Resources/Orthanc/CMake/GoogleTestConfiguration.cmake
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
if (USE_GOOGLE_TEST_DEBIAN_PACKAGE)
|
||||||
|
find_path(GOOGLE_TEST_DEBIAN_SOURCES_DIR
|
||||||
|
NAMES src/gtest-all.cc
|
||||||
|
PATHS
|
||||||
|
${CROSSTOOL_NG_IMAGE}/usr/src/gtest
|
||||||
|
${CROSSTOOL_NG_IMAGE}/usr/src/googletest/googletest
|
||||||
|
PATH_SUFFIXES src
|
||||||
|
)
|
||||||
|
|
||||||
|
find_path(GOOGLE_TEST_DEBIAN_INCLUDE_DIR
|
||||||
|
NAMES gtest.h
|
||||||
|
PATHS
|
||||||
|
${CROSSTOOL_NG_IMAGE}/usr/include/gtest
|
||||||
|
)
|
||||||
|
|
||||||
|
message("Path to the Debian Google Test sources: ${GOOGLE_TEST_DEBIAN_SOURCES_DIR}")
|
||||||
|
message("Path to the Debian Google Test includes: ${GOOGLE_TEST_DEBIAN_INCLUDE_DIR}")
|
||||||
|
|
||||||
|
set(GOOGLE_TEST_SOURCES
|
||||||
|
${GOOGLE_TEST_DEBIAN_SOURCES_DIR}/src/gtest-all.cc
|
||||||
|
)
|
||||||
|
|
||||||
|
include_directories(${GOOGLE_TEST_DEBIAN_SOURCES_DIR})
|
||||||
|
|
||||||
|
if (NOT EXISTS ${GOOGLE_TEST_SOURCES} OR
|
||||||
|
NOT EXISTS ${GOOGLE_TEST_DEBIAN_INCLUDE_DIR}/gtest.h)
|
||||||
|
message(FATAL_ERROR "Please install the libgtest-dev package")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
elseif (STATIC_BUILD OR NOT USE_SYSTEM_GOOGLE_TEST)
|
||||||
|
set(GOOGLE_TEST_SOURCES_DIR ${CMAKE_BINARY_DIR}/googletest-release-1.8.1)
|
||||||
|
set(GOOGLE_TEST_URL "https://orthanc.uclouvain.be/downloads/third-party-downloads/gtest-1.8.1.tar.gz")
|
||||||
|
set(GOOGLE_TEST_MD5 "2e6fbeb6a91310a16efe181886c59596")
|
||||||
|
|
||||||
|
DownloadPackage(${GOOGLE_TEST_MD5} ${GOOGLE_TEST_URL} "${GOOGLE_TEST_SOURCES_DIR}")
|
||||||
|
|
||||||
|
include_directories(
|
||||||
|
${GOOGLE_TEST_SOURCES_DIR}/googletest
|
||||||
|
${GOOGLE_TEST_SOURCES_DIR}/googletest/include
|
||||||
|
${GOOGLE_TEST_SOURCES_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
set(GOOGLE_TEST_SOURCES
|
||||||
|
${GOOGLE_TEST_SOURCES_DIR}/googletest/src/gtest-all.cc
|
||||||
|
)
|
||||||
|
|
||||||
|
# https://code.google.com/p/googletest/issues/detail?id=412
|
||||||
|
if (MSVC) # VS2012 does not support tuples correctly yet
|
||||||
|
add_definitions(/D _VARIADIC_MAX=10)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
|
||||||
|
add_definitions(-DGTEST_HAS_CLONE=0)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
source_group(ThirdParty\\GoogleTest REGULAR_EXPRESSION ${GOOGLE_TEST_SOURCES_DIR}/.*)
|
||||||
|
|
||||||
|
else()
|
||||||
|
include(FindGTest)
|
||||||
|
if (NOT GTEST_FOUND)
|
||||||
|
message(FATAL_ERROR "Unable to find GoogleTest")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
include_directories(${GTEST_INCLUDE_DIRS})
|
||||||
|
|
||||||
|
# The variable GTEST_LIBRARIES contains the shared library of
|
||||||
|
# Google Test, create an alias for more uniformity
|
||||||
|
set(GOOGLE_TEST_LIBRARIES ${GTEST_LIBRARIES})
|
||||||
|
endif()
|
||||||
7
Resources/Orthanc/Plugins/ExportedSymbolsPlugins.list
Normal file
7
Resources/Orthanc/Plugins/ExportedSymbolsPlugins.list
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# This is the list of the symbols that must be exported by Orthanc
|
||||||
|
# plugins, if targeting OS X
|
||||||
|
|
||||||
|
_OrthancPluginInitialize
|
||||||
|
_OrthancPluginFinalize
|
||||||
|
_OrthancPluginGetName
|
||||||
|
_OrthancPluginGetVersion
|
||||||
4117
Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
Normal file
4117
Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp
Normal file
File diff suppressed because it is too large
Load Diff
1511
Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h
Normal file
1511
Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h
Normal file
File diff suppressed because it is too large
Load Diff
91
Resources/Orthanc/Plugins/OrthancPluginException.h
Normal file
91
Resources/Orthanc/Plugins/OrthancPluginException.h
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
* Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
* Department, University Hospital of Liege, Belgium
|
||||||
|
* Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
* Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
* Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU 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
|
||||||
|
* General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
**/
|
||||||
|
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#if !defined(HAS_ORTHANC_EXCEPTION)
|
||||||
|
# error The macro HAS_ORTHANC_EXCEPTION must be defined
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#if HAS_ORTHANC_EXCEPTION == 1
|
||||||
|
# include <OrthancException.h>
|
||||||
|
# define ORTHANC_PLUGINS_ERROR_ENUMERATION ::Orthanc::ErrorCode
|
||||||
|
# define ORTHANC_PLUGINS_EXCEPTION_CLASS ::Orthanc::OrthancException
|
||||||
|
# define ORTHANC_PLUGINS_GET_ERROR_CODE(code) ::Orthanc::ErrorCode_ ## code
|
||||||
|
#else
|
||||||
|
# include <orthanc/OrthancCPlugin.h>
|
||||||
|
# define ORTHANC_PLUGINS_ERROR_ENUMERATION ::OrthancPluginErrorCode
|
||||||
|
# define ORTHANC_PLUGINS_EXCEPTION_CLASS ::OrthancPlugins::PluginException
|
||||||
|
# define ORTHANC_PLUGINS_GET_ERROR_CODE(code) ::OrthancPluginErrorCode_ ## code
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
#define ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(code) \
|
||||||
|
throw ORTHANC_PLUGINS_EXCEPTION_CLASS(static_cast<ORTHANC_PLUGINS_ERROR_ENUMERATION>(code));
|
||||||
|
|
||||||
|
|
||||||
|
#define ORTHANC_PLUGINS_THROW_EXCEPTION(code) \
|
||||||
|
throw ORTHANC_PLUGINS_EXCEPTION_CLASS(ORTHANC_PLUGINS_GET_ERROR_CODE(code));
|
||||||
|
|
||||||
|
|
||||||
|
#define ORTHANC_PLUGINS_CHECK_ERROR(code) \
|
||||||
|
if (code != ORTHANC_PLUGINS_GET_ERROR_CODE(Success)) \
|
||||||
|
{ \
|
||||||
|
ORTHANC_PLUGINS_THROW_EXCEPTION(code); \
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
namespace OrthancPlugins
|
||||||
|
{
|
||||||
|
#if HAS_ORTHANC_EXCEPTION == 0
|
||||||
|
class PluginException
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
OrthancPluginErrorCode code_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit PluginException(OrthancPluginErrorCode code) : code_(code)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
OrthancPluginErrorCode GetErrorCode() const
|
||||||
|
{
|
||||||
|
return code_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* What(OrthancPluginContext* context) const
|
||||||
|
{
|
||||||
|
const char* description = OrthancPluginGetErrorDescription(context, code_);
|
||||||
|
if (description)
|
||||||
|
{
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return "No description available";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
}
|
||||||
33
Resources/Orthanc/Plugins/OrthancPluginsExports.cmake
Normal file
33
Resources/Orthanc/Plugins/OrthancPluginsExports.cmake
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU 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
|
||||||
|
# General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
# In Orthanc <= 1.7.1, the instructions below were part of
|
||||||
|
# "Compiler.cmake", and were protected by the (now unused) option
|
||||||
|
# "ENABLE_PLUGINS_VERSION_SCRIPT" in CMake
|
||||||
|
|
||||||
|
if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
|
||||||
|
${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
|
||||||
|
${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
|
||||||
|
${CMAKE_SYSTEM_NAME} STREQUAL "OpenBSD")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--version-script=${CMAKE_CURRENT_LIST_DIR}/VersionScriptPlugins.map")
|
||||||
|
elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
|
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -exported_symbols_list ${CMAKE_CURRENT_LIST_DIR}/ExportedSymbolsPlugins.list")
|
||||||
|
endif()
|
||||||
12
Resources/Orthanc/Plugins/VersionScriptPlugins.map
Normal file
12
Resources/Orthanc/Plugins/VersionScriptPlugins.map
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# This is a version-script for Orthanc plugins
|
||||||
|
|
||||||
|
{
|
||||||
|
global:
|
||||||
|
OrthancPluginInitialize;
|
||||||
|
OrthancPluginFinalize;
|
||||||
|
OrthancPluginGetName;
|
||||||
|
OrthancPluginGetVersion;
|
||||||
|
|
||||||
|
local:
|
||||||
|
*;
|
||||||
|
};
|
||||||
2
Resources/Orthanc/README.txt
Normal file
2
Resources/Orthanc/README.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
This folder contains an excerpt of the source code of Orthanc. It is
|
||||||
|
automatically generated using the "../SyncOrthancFolder.py" script.
|
||||||
9043
Resources/Orthanc/Sdk-1.11.2/orthanc/OrthancCPlugin.h
Normal file
9043
Resources/Orthanc/Sdk-1.11.2/orthanc/OrthancCPlugin.h
Normal file
File diff suppressed because it is too large
Load Diff
101
Resources/Orthanc/Toolchains/LinuxStandardBaseToolchain.cmake
Normal file
101
Resources/Orthanc/Toolchains/LinuxStandardBaseToolchain.cmake
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Full build, as used on the BuildBot CIS:
|
||||||
|
#
|
||||||
|
# $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake ../OrthancServer/ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_LIBICU=ON -DUSE_LEGACY_BOOST=ON -DBOOST_LOCALE_BACKEND=icu -DENABLE_PKCS11=ON -G Ninja
|
||||||
|
#
|
||||||
|
# Or, more lightweight version (without libp11 and ICU):
|
||||||
|
#
|
||||||
|
# $ LSB_CC=gcc-4.8 LSB_CXX=g++-4.8 cmake ../OrthancServer/ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=../OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake -DUSE_LEGACY_JSONCPP=ON -DUSE_LEGACY_BOOST=ON -G Ninja
|
||||||
|
#
|
||||||
|
|
||||||
|
INCLUDE(CMakeForceCompiler)
|
||||||
|
|
||||||
|
SET(LSB_PATH $ENV{LSB_PATH} CACHE STRING "")
|
||||||
|
SET(LSB_CC $ENV{LSB_CC} CACHE STRING "")
|
||||||
|
SET(LSB_CXX $ENV{LSB_CXX} CACHE STRING "")
|
||||||
|
SET(LSB_TARGET_VERSION "4.0" CACHE STRING "")
|
||||||
|
|
||||||
|
IF ("${LSB_PATH}" STREQUAL "")
|
||||||
|
SET(LSB_PATH "/opt/lsb")
|
||||||
|
ENDIF()
|
||||||
|
|
||||||
|
IF (EXISTS ${LSB_PATH}/lib64)
|
||||||
|
SET(LSB_TARGET_PROCESSOR "x86_64")
|
||||||
|
SET(LSB_LIBPATH ${LSB_PATH}/lib64-${LSB_TARGET_VERSION})
|
||||||
|
ELSEIF (EXISTS ${LSB_PATH}/lib)
|
||||||
|
SET(LSB_TARGET_PROCESSOR "x86")
|
||||||
|
SET(LSB_LIBPATH ${LSB_PATH}/lib-${LSB_TARGET_VERSION})
|
||||||
|
ELSE()
|
||||||
|
MESSAGE(FATAL_ERROR "Unable to detect the target processor architecture. Check the LSB_PATH environment variable.")
|
||||||
|
ENDIF()
|
||||||
|
|
||||||
|
SET(LSB_CPPPATH ${LSB_PATH}/include)
|
||||||
|
SET(PKG_CONFIG_PATH ${LSB_LIBPATH}/pkgconfig/)
|
||||||
|
|
||||||
|
# the name of the target operating system
|
||||||
|
SET(CMAKE_SYSTEM_NAME Linux)
|
||||||
|
SET(CMAKE_SYSTEM_VERSION LinuxStandardBase)
|
||||||
|
SET(CMAKE_SYSTEM_PROCESSOR ${LSB_TARGET_PROCESSOR})
|
||||||
|
|
||||||
|
# which compilers to use for C and C++
|
||||||
|
SET(CMAKE_C_COMPILER ${LSB_PATH}/bin/lsbcc)
|
||||||
|
|
||||||
|
if (${CMAKE_VERSION} VERSION_LESS "3.6.0")
|
||||||
|
CMAKE_FORCE_CXX_COMPILER(${LSB_PATH}/bin/lsbc++ GNU)
|
||||||
|
else()
|
||||||
|
SET(CMAKE_CXX_COMPILER ${LSB_PATH}/bin/lsbc++)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# here is the target environment located
|
||||||
|
SET(CMAKE_FIND_ROOT_PATH ${LSB_PATH})
|
||||||
|
|
||||||
|
# adjust the default behaviour of the FIND_XXX() commands:
|
||||||
|
# search headers and libraries in the target environment, search
|
||||||
|
# programs in the host environment
|
||||||
|
SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY NEVER)
|
||||||
|
SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE NEVER)
|
||||||
|
|
||||||
|
SET(CMAKE_CROSSCOMPILING OFF)
|
||||||
|
|
||||||
|
|
||||||
|
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -I${LSB_PATH}/include" CACHE INTERNAL "" FORCE)
|
||||||
|
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -nostdinc++ -I${LSB_PATH}/include -I${LSB_PATH}/include/c++ -I${LSB_PATH}/include/c++/backward" CACHE INTERNAL "" FORCE)
|
||||||
|
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH} --lsb-besteffort" CACHE INTERNAL "" FORCE)
|
||||||
|
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-target-version=${LSB_TARGET_VERSION} -L${LSB_LIBPATH} --lsb-besteffort" CACHE INTERNAL "" FORCE)
|
||||||
|
|
||||||
|
if (NOT "${LSB_CXX}" STREQUAL "")
|
||||||
|
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-cxx=${LSB_CXX}")
|
||||||
|
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-cxx=${LSB_CXX}")
|
||||||
|
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-cxx=${LSB_CXX}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if (NOT "${LSB_CC}" STREQUAL "")
|
||||||
|
SET(CMAKE_C_FLAGS "${CMAKE_CC_FLAGS} --lsb-cc=${LSB_CC}")
|
||||||
|
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --lsb-cc=${LSB_CC}")
|
||||||
|
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} --lsb-cc=${LSB_CC}")
|
||||||
|
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --lsb-cc=${LSB_CC}")
|
||||||
|
endif()
|
||||||
|
|
||||||
39
Resources/Orthanc/Toolchains/MinGW-W64-Toolchain32.cmake
Normal file
39
Resources/Orthanc/Toolchains/MinGW-W64-Toolchain32.cmake
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
# the name of the target operating system
|
||||||
|
set(CMAKE_SYSTEM_NAME Windows)
|
||||||
|
|
||||||
|
# which compilers to use for C and C++
|
||||||
|
set(CMAKE_C_COMPILER i686-w64-mingw32-gcc)
|
||||||
|
set(CMAKE_CXX_COMPILER i686-w64-mingw32-g++)
|
||||||
|
set(CMAKE_RC_COMPILER i686-w64-mingw32-windres)
|
||||||
|
|
||||||
|
# here is the target environment located
|
||||||
|
set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
|
||||||
|
|
||||||
|
# adjust the default behaviour of the FIND_XXX() commands:
|
||||||
|
# search headers and libraries in the target environment, search
|
||||||
|
# programs in the host environment
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
39
Resources/Orthanc/Toolchains/MinGW-W64-Toolchain64.cmake
Normal file
39
Resources/Orthanc/Toolchains/MinGW-W64-Toolchain64.cmake
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
# the name of the target operating system
|
||||||
|
set(CMAKE_SYSTEM_NAME Windows)
|
||||||
|
|
||||||
|
# which compilers to use for C and C++
|
||||||
|
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc)
|
||||||
|
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++)
|
||||||
|
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||||
|
|
||||||
|
# here is the target environment located
|
||||||
|
set(CMAKE_FIND_ROOT_PATH /usr/i686-w64-mingw32)
|
||||||
|
|
||||||
|
# adjust the default behaviour of the FIND_XXX() commands:
|
||||||
|
# search headers and libraries in the target environment, search
|
||||||
|
# programs in the host environment
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
42
Resources/Orthanc/Toolchains/MinGWToolchain.cmake
Normal file
42
Resources/Orthanc/Toolchains/MinGWToolchain.cmake
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Orthanc - A Lightweight, RESTful DICOM Store
|
||||||
|
# Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
||||||
|
# Department, University Hospital of Liege, Belgium
|
||||||
|
# Copyright (C) 2017-2023 Osimis S.A., Belgium
|
||||||
|
# Copyright (C) 2024-2024 Orthanc Team SRL, Belgium
|
||||||
|
# Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program. If not, see
|
||||||
|
# <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
# the name of the target operating system
|
||||||
|
set(CMAKE_SYSTEM_NAME Windows)
|
||||||
|
|
||||||
|
# which compilers to use for C and C++
|
||||||
|
set(CMAKE_C_COMPILER i586-mingw32msvc-gcc)
|
||||||
|
set(CMAKE_CXX_COMPILER i586-mingw32msvc-g++)
|
||||||
|
set(CMAKE_RC_COMPILER i586-mingw32msvc-windres)
|
||||||
|
|
||||||
|
# here is the target environment located
|
||||||
|
set(CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc)
|
||||||
|
|
||||||
|
# adjust the default behaviour of the FIND_XXX() commands:
|
||||||
|
# search headers and libraries in the target environment, search
|
||||||
|
# programs in the host environment
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||||
|
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||||
|
|
||||||
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DSTACK_SIZE_PARAM_IS_A_RESERVATION=0x10000" CACHE INTERNAL "" FORCE)
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DSTACK_SIZE_PARAM_IS_A_RESERVATION=0x10000" CACHE INTERNAL "" FORCE)
|
||||||
78
Resources/SyncOrthancFolder.py
Normal file
78
Resources/SyncOrthancFolder.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
#
|
||||||
|
# This maintenance script updates the content of the "Orthanc" folder
|
||||||
|
# to match the latest version of the Orthanc source code.
|
||||||
|
#
|
||||||
|
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
TARGET = os.path.join(os.path.dirname(__file__), 'Orthanc')
|
||||||
|
PLUGIN_SDK_VERSION = '1.11.2'
|
||||||
|
REPOSITORY = 'https://orthanc.uclouvain.be/hg/orthanc/raw-file'
|
||||||
|
|
||||||
|
FILES = [
|
||||||
|
('OrthancFramework/Resources/CMake/AutoGeneratedCode.cmake', 'CMake'),
|
||||||
|
('OrthancFramework/Resources/CMake/Compiler.cmake', 'CMake'),
|
||||||
|
('OrthancFramework/Resources/CMake/DownloadOrthancFramework.cmake', 'CMake'),
|
||||||
|
('OrthancFramework/Resources/CMake/DownloadPackage.cmake', 'CMake'),
|
||||||
|
('OrthancFramework/Resources/CMake/GoogleTestConfiguration.cmake', 'CMake'),
|
||||||
|
('OrthancFramework/Resources/EmbedResources.py', 'CMake'),
|
||||||
|
('OrthancFramework/Resources/Toolchains/LinuxStandardBaseToolchain.cmake', 'Toolchains'),
|
||||||
|
('OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain32.cmake', 'Toolchains'),
|
||||||
|
('OrthancFramework/Resources/Toolchains/MinGW-W64-Toolchain64.cmake', 'Toolchains'),
|
||||||
|
('OrthancFramework/Resources/Toolchains/MinGWToolchain.cmake', 'Toolchains'),
|
||||||
|
('OrthancServer/Plugins/Samples/Common/ExportedSymbolsPlugins.list', 'Plugins'),
|
||||||
|
('OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp', 'Plugins'),
|
||||||
|
('OrthancServer/Plugins/Samples/Common/OrthancPluginCppWrapper.h', 'Plugins'),
|
||||||
|
('OrthancServer/Plugins/Samples/Common/OrthancPluginException.h', 'Plugins'),
|
||||||
|
('OrthancServer/Plugins/Samples/Common/OrthancPluginsExports.cmake', 'Plugins'),
|
||||||
|
('OrthancServer/Plugins/Samples/Common/VersionScriptPlugins.map', 'Plugins'),
|
||||||
|
]
|
||||||
|
|
||||||
|
SDK = [
|
||||||
|
'orthanc/OrthancCPlugin.h',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def Download(x):
|
||||||
|
branch = x[0]
|
||||||
|
source = x[1]
|
||||||
|
target = os.path.join(TARGET, x[2])
|
||||||
|
print(target)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(target))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
url = '%s/%s/%s' % (REPOSITORY, branch, source)
|
||||||
|
|
||||||
|
with open(target, 'wb') as f:
|
||||||
|
try:
|
||||||
|
f.write(urllib.request.urlopen(url).read())
|
||||||
|
except:
|
||||||
|
print('ERROR %s' % url)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
for f in FILES:
|
||||||
|
commands.append([ 'default',
|
||||||
|
f[0],
|
||||||
|
os.path.join(f[1], os.path.basename(f[0])) ])
|
||||||
|
|
||||||
|
for f in SDK:
|
||||||
|
commands.append([
|
||||||
|
'Orthanc-%s' % PLUGIN_SDK_VERSION,
|
||||||
|
'OrthancServer/Plugins/Include/%s' % f,
|
||||||
|
'Sdk-%s/%s' % (PLUGIN_SDK_VERSION, f)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
pool = multiprocessing.Pool(10) # simultaneous downloads
|
||||||
|
pool.map(Download, commands)
|
||||||
98
TODO
Normal file
98
TODO
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
- implement pagination to fetch studies 100 -> 200 when we scroll to the end of the study-list
|
||||||
|
- implement pagination to fetch instances 100 -> 200 when we scroll to the end of the instance-list (displaying 2000 instances in one go sometimes takes 10 seconds !)
|
||||||
|
|
||||||
|
|
||||||
|
- add a "reset" button (on Windows, to reload the config after you have changed it without going to the Services -> Orthanc -> Restart)
|
||||||
|
- show ohif-vr and ohif-tmtv buttons only when relevant (analyse the content of the study)
|
||||||
|
|
||||||
|
- predefined filters in config file to display below Studies:
|
||||||
|
- "Today": {"StudyDate": "$today"}
|
||||||
|
- "CT Last month": {"ModalitiesInStudy": "CT", "StudyDate": "$oneMonthAgo-"}
|
||||||
|
predefined keywords: $today, $yesterday, $oneWeekAgo, $oneMonthAgo, $oneYearAgo
|
||||||
|
|
||||||
|
UI improvements:
|
||||||
|
- Admin theme including responsive tables (including multiline): https://github.com/lekoala/admini
|
||||||
|
- tags: https://github.com/lekoala/bootstrap5-tags
|
||||||
|
- when opening the series view I would prefer to see an image and it’s labels over a list of instance numbers and paired SOPInstanceUIDs:
|
||||||
|
https://discourse.orthanc-server.org/t/beginner-questions-from-horos-user/5322
|
||||||
|
|
||||||
|
- modification:
|
||||||
|
- configuration to hide DICOM UID options and select the right one directly for a given setup.
|
||||||
|
- add a Series Description renamer (one dialog to edit SeriesNumber + SeriesDescription of a study)
|
||||||
|
|
||||||
|
- settings:
|
||||||
|
- show the list of /tools/accepted-transfer-syntaxes
|
||||||
|
- show the list of /tools/accepted-sop-classes (in 1.12.6)
|
||||||
|
|
||||||
|
- show attachments
|
||||||
|
|
||||||
|
- support neuro plugin (download nifti)
|
||||||
|
|
||||||
|
- TagsTree: allow click on "null" tags to open /instances/../content/group,element in new window
|
||||||
|
|
||||||
|
- show job details (need improvement in Orthanc API itself)
|
||||||
|
- include an "all jobs" panel and not only our jobs (https://discourse.orthanc-server.org/t/oe2-inclusion-of-jobs-panel-in-explorer-2/3708)
|
||||||
|
|
||||||
|
- list last studies received (through DICOM or upload)
|
||||||
|
|
||||||
|
- UI customization:
|
||||||
|
- add custom actions per study/series/instances:
|
||||||
|
- configuration suggestion:
|
||||||
|
{
|
||||||
|
"CustomButtons": [
|
||||||
|
{
|
||||||
|
"Name": "Process study", // the text will appear in the button tooltip
|
||||||
|
"Icon": "bi bi-glass",
|
||||||
|
"Level": "Study",
|
||||||
|
"ActionUrl": "../../my-python-plugin/my-route", // this route can be templated with e.g: ../../my-python-plugin/my-route?StudyInstanceUID=$resource-dicom-id$&orthanc-id=$resource-orthanc-id$&level=$resource-level$"
|
||||||
|
"ActionMethod": "POST",
|
||||||
|
"ActionPostPayloadTemplate": {
|
||||||
|
"dicom-id": "$resource-dicom-id$",
|
||||||
|
"orthanc-id": "$resource-orthanc-id$"
|
||||||
|
}
|
||||||
|
"EnabledUrl": "../../my-python-plugin/my-route-enabled", // optional: a GET route that returns true/false if the custom button must be enabled/disabled for this resource. It shall be templated as well
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
- configure other viewers url (ex: radiant://?n=pstv&v=0020000D&v=%22StudyInstanceUID%22 or osirix or horos ...)
|
||||||
|
|
||||||
|
- orthanc-share should generate QR code with publication links
|
||||||
|
|
||||||
|
- Q&R on multiple modalities at a same time (select the modalities you want to Q&R and display the modality in the study list)
|
||||||
|
(same with Q&R for dicom-web and peers)
|
||||||
|
|
||||||
|
- in local study list, display the number of studies that are present on a remote modality for this patient (e.g: a cold archive)
|
||||||
|
- Add a button to fetch all these data (todo: find a way to delete them after a while ?)
|
||||||
|
- same with dicom-web and peers ?
|
||||||
|
|
||||||
|
|
||||||
|
Configuration management (ideas + implementation notes):
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
- allow Orthanc to store anything in Global Properties. Maybe not through the API but only through the SDK.
|
||||||
|
Or, only for an "admin" user ?
|
||||||
|
|
||||||
|
- Edit configuration through the oe2 UI:
|
||||||
|
- Some immutable configuration in a config file:
|
||||||
|
- DB + storage
|
||||||
|
- Admin user !
|
||||||
|
- HttpPort
|
||||||
|
- Check if we could store a config in Global Properties (probably too early in the Orthanc init process). We would merge the file config with the config in DB
|
||||||
|
- Otherwise, store the config in a file and restart Orthanc
|
||||||
|
-> in Windows Service: how to tell Orthanc to use another config file ?
|
||||||
|
-> would be nice to have a SDK route to implement /tools/reboot with a given config file
|
||||||
|
- Could be a combination of 2:
|
||||||
|
- start Orthanc with config from file
|
||||||
|
- plugin reads the config in DB, generates a tmp config file -> /tools/reboot with this file
|
||||||
|
- that's a bit "shaky" since each cold start is made of 2 starts
|
||||||
|
|
||||||
|
Ideas bag:
|
||||||
|
*********
|
||||||
|
|
||||||
|
- allow users to choose the columns in the interface (store in browser LocalStorage ?)
|
||||||
|
- browse orthanc peer (probably need to extend the Orthanc API to avoid CORS issues)
|
||||||
|
- show statistics/event logs: e.g: would be nice to see how many instances
|
||||||
|
have been received recently (from where)
|
||||||
|
- add a button to export a whole series to jpeg (Chamrousse): we could reuse [this python code](https://orthanc.uclouvain.be/book/plugins/python.html#generating-a-mosaic-for-a-dicom-series) and trigger it from a [custom button](https://github.com/orthanc-server/orthanc-explorer-2/issues/18) (once we have implemented them !!!)
|
||||||
|
- add a text editor to associate a radiology report or note with a study, storing it as an attachment (cf. https://discourse.orthanc-server.org/t/oe2-inclusion-of-reporting-note/)
|
||||||
24
WebApplication/index.html
Normal file
24
WebApplication/index.html
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-bs-theme="light"><!-- don't change !!! data-bs-theme is changed dynamically in the C++ code when the index.html file is loaded or when the custom css is loaded for dev setups-->
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>...</title>
|
||||||
|
<script>
|
||||||
|
// Custom bit of code to redirect to /app/ if the uri is /app
|
||||||
|
// The reason for this is that the path to assets is relative
|
||||||
|
// so it won't work for /app but it will for /app/
|
||||||
|
const currentUri = new URL(window.location.href);
|
||||||
|
|
||||||
|
if (currentUri.pathname.endsWith("app")) {
|
||||||
|
currentUri.pathname += "/";
|
||||||
|
window.location.href = currentUri.toString();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1341
WebApplication/package-lock.json
generated
Normal file
1341
WebApplication/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
WebApplication/package.json
Normal file
33
WebApplication/package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "orthanc-explorer-2",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||||
|
"@popperjs/core": "^2.11.8",
|
||||||
|
"@vuepic/vue-datepicker": "^8.3.1",
|
||||||
|
"axios": "^1.7.4",
|
||||||
|
"bootstrap": "5.3.3",
|
||||||
|
"bootstrap-icons": "^1.11.3",
|
||||||
|
"bootstrap5-tags": "^1.7.2",
|
||||||
|
"jquery": "^3.7.1",
|
||||||
|
"keycloak-js": "^24.0.5",
|
||||||
|
"mitt": "^3.0.1",
|
||||||
|
"uppie": "^4.0.0",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"vue": "^3.4.21",
|
||||||
|
"vue-i18n": "^9.10.2",
|
||||||
|
"vue-router": "^4.3.0",
|
||||||
|
"vuex": "^4.1.0",
|
||||||
|
"vue3-observe-visibility": "^1.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
|
"vite": "^5.4.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
WebApplication/public/favicon.ico
Normal file
BIN
WebApplication/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
13
WebApplication/retrieve-and-view.html
Normal file
13
WebApplication/retrieve-and-view.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>OE2 Retrieve and view</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app-retrieve-and-view"></div>
|
||||||
|
<script type="module" src="/src/main-retrieve-and-view.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
84
WebApplication/src/App.vue
Normal file
84
WebApplication/src/App.vue
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<script>
|
||||||
|
function applyBootStrapTheme() {
|
||||||
|
// hack to switch the theme: get the value from our custom css
|
||||||
|
let bootstrapTheme = getComputedStyle(document.documentElement).getPropertyValue('--bootstrap-theme');
|
||||||
|
console.log("-------------- Applying Bootstrap theme ...", bootstrapTheme);
|
||||||
|
if (bootstrapTheme) {
|
||||||
|
// and set it to the 'html' element
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', bootstrapTheme);
|
||||||
|
} else {
|
||||||
|
console.warn("-------------- Applying Bootstrap theme not defined yet, retrying ...");
|
||||||
|
setTimeout(applyBootStrapTheme, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async created() {
|
||||||
|
console.log("Creating App...");
|
||||||
|
|
||||||
|
{// Load the CSS dynamically since it can be a custom css
|
||||||
|
console.log("Loading the defaults + custom CSS ...");
|
||||||
|
|
||||||
|
let link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.type = 'text/css';
|
||||||
|
link.href = 'customizable/custom.css';
|
||||||
|
|
||||||
|
document.getElementsByTagName('HEAD')[0].appendChild(link);
|
||||||
|
|
||||||
|
setTimeout(applyBootStrapTheme, 0);
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// console.log("Loading the custom CSS ...");
|
||||||
|
|
||||||
|
// let link = document.createElement('link');
|
||||||
|
// link.rel = 'stylesheet';
|
||||||
|
// link.type = 'text/css';
|
||||||
|
// link.href = 'customizable/custom.css';
|
||||||
|
|
||||||
|
// document.getElementsByTagName('HEAD')[0].appendChild(link);
|
||||||
|
// }
|
||||||
|
|
||||||
|
await this.$store.dispatch('configuration/load');
|
||||||
|
await this.$store.dispatch('studies/initialLoad');
|
||||||
|
await this.$store.dispatch('labels/refresh');
|
||||||
|
console.log("App created");
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="full-page">
|
||||||
|
<div class="nav-side-layout">
|
||||||
|
<router-view name="SideBarView"></router-view>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<router-view name="ContentView"></router-view>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-layout {
|
||||||
|
overflow: auto;
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
width: var(--nav-bar-width);
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--nav-side-bg-color);
|
||||||
|
color: var(--nav-side-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-layout .toggle-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
47
WebApplication/src/AppLanding.vue
Normal file
47
WebApplication/src/AppLanding.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<script>
|
||||||
|
import api from "./orthancApi"
|
||||||
|
import TokenLanding from "./components/TokenLanding.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async created() {
|
||||||
|
console.log("Creating Landing App...");
|
||||||
|
// await this.$store.dispatch('configuration/load');
|
||||||
|
// await this.$store.dispatch('studies/initialLoad');
|
||||||
|
console.log("Landing App created");
|
||||||
|
},
|
||||||
|
components: { TokenLanding }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="full-page">
|
||||||
|
<TokenLanding>
|
||||||
|
</TokenLanding>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-align: center;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-layout {
|
||||||
|
overflow: auto;
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
width: var(--nav-bar-width);
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--nav-side-bg-color);
|
||||||
|
color: var(--nav-side-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-layout .toggle-btn {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
33
WebApplication/src/AppRetrieveAndView.vue
Normal file
33
WebApplication/src/AppRetrieveAndView.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script>
|
||||||
|
import api from "./orthancApi"
|
||||||
|
import RetrieveAndView from "./components/RetrieveAndView.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async created() {
|
||||||
|
console.log("Creating RetrieveAndView App...");
|
||||||
|
await this.$store.dispatch('configuration/load');
|
||||||
|
// await this.$store.dispatch('studies/initialLoad');
|
||||||
|
console.log("RetrieveAndView App created");
|
||||||
|
},
|
||||||
|
components: { RetrieveAndView }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="full-page">
|
||||||
|
<RetrieveAndView>
|
||||||
|
</RetrieveAndView>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#app {
|
||||||
|
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
text-align: center;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
75
WebApplication/src/assets/css/common.css
Normal file
75
WebApplication/src/assets/css/common.css
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
.cut-text {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre; /* preserve double white spaces (https://github.com/orthanc-server/orthanc-explorer-2/issues/30) */
|
||||||
|
max-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* override CSS from datepicker to avoid the close button to overflow over the text */
|
||||||
|
.dp__input_icon_pad {
|
||||||
|
padding-right: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__preset_ranges {
|
||||||
|
width: 300px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__range_end, .dp__range_start, .dp__active_date {
|
||||||
|
background: var(--dp-primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* for the StudyItem */
|
||||||
|
.label {
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-left: 2px;
|
||||||
|
background-color: var(--label-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* for the tag editor */
|
||||||
|
.tags-badge.bg-info {
|
||||||
|
background-color: var(--label-bg-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||||
|
color: var(--form-placeholder-color);
|
||||||
|
opacity: 1; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-card {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-card a {
|
||||||
|
/* color: black; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-card-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.job-pct-pill {
|
||||||
|
width: 50px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jobs-header {
|
||||||
|
text-align: left;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
line-height: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jobs-body {
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
line-height: 1.6;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jobs-body a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
67
WebApplication/src/assets/css/defaults-dark.css
Normal file
67
WebApplication/src/assets/css/defaults-dark.css
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/* Customized CSS.
|
||||||
|
This CSS is loaded after all other CSS files have been loaded
|
||||||
|
therefore, it overrides all other definitions */
|
||||||
|
:root {
|
||||||
|
--bootstrap-theme: dark;
|
||||||
|
|
||||||
|
--study-even-bg-color: rgb(55, 55, 60);
|
||||||
|
--study-odd-bg-color: rgb(45, 45, 50);
|
||||||
|
--study-hover-color: rgb(100, 98, 95);
|
||||||
|
--study-selected-color: rgb(40, 40, 45);
|
||||||
|
--study-details-bg-color: var(--study-selected-color);
|
||||||
|
--study-list-remote-bg-color: var(--study-hover-color);
|
||||||
|
|
||||||
|
--series-even-bg-color: rgb(45, 45, 50);
|
||||||
|
--series-odd-bg-color: rgb(35, 35, 40);
|
||||||
|
--series-hover-color: var(--study-hover-color);
|
||||||
|
--series-selected-color: rgb(30, 30, 35);
|
||||||
|
--series-details-bg-color: var(--series-selected-color);
|
||||||
|
|
||||||
|
--instance-odd-bg-color: rgb(35, 35, 40);
|
||||||
|
--instance-even-bg-color: rgb(30, 30, 35);
|
||||||
|
--instance-hover-color: var(--study-hover-color);
|
||||||
|
--instance-selected-color: rgb(20, 20, 25);
|
||||||
|
--instance-details-bg-color: var(--instance-selected-color);
|
||||||
|
|
||||||
|
--study-table-header-bg-color: var(--study-even-bg-color);
|
||||||
|
--study-table-filter-bg-color: rgb(60, 60, 59);
|
||||||
|
--study-table-actions-bg-color: var(--study-table-filter-bg-color);
|
||||||
|
|
||||||
|
--table-filters-is-searching-color: #fda90d86;
|
||||||
|
--table-filters-is-not-searching-color: rgb(30, 150, 255);
|
||||||
|
|
||||||
|
--details-top-margin: 25px;
|
||||||
|
|
||||||
|
--nav-bar-width: 260px;
|
||||||
|
|
||||||
|
--nav-side-color: rgb(255, 255, 255);
|
||||||
|
--nav-side-bg-color: rgb(30, 36, 42);
|
||||||
|
--nav-side-sub-bg-color: #181c20;
|
||||||
|
--nav-side-selected-bg-color: #4f5b69;
|
||||||
|
--nav-side-hover-bg-color: #3e454d;
|
||||||
|
--nav-side-submenu-color: rgb(255, 255, 255);
|
||||||
|
--nav-side-active-border-color: #d19b3d;
|
||||||
|
|
||||||
|
--label-bg-color: rgb(61, 116, 141);
|
||||||
|
|
||||||
|
--form-placeholder-color: #888;
|
||||||
|
|
||||||
|
/* you may also customize global bootstrap variables */
|
||||||
|
--bs-border-color: rgb(110, 160, 200);
|
||||||
|
/* --bs-info-rgb: 255, 26, 35; */
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* datepicker variables */
|
||||||
|
.dp__theme_dark {
|
||||||
|
--dp-border-color: var(--bs-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* optional, you may e.g. customize bootstrap buttons or any other class that is declared globally */
|
||||||
|
|
||||||
|
/*
|
||||||
|
.btn-primary {
|
||||||
|
--bs-btn-color: white;
|
||||||
|
--bs-btn-bg: red;
|
||||||
|
} */
|
||||||
|
|
||||||
72
WebApplication/src/assets/css/defaults-light.css
Normal file
72
WebApplication/src/assets/css/defaults-light.css
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/* Default CSS.
|
||||||
|
This CSS is loaded after all other CSS files have been loaded
|
||||||
|
therefore, it overrides all other definitions. However, if you provide
|
||||||
|
a CustomCssPath, the custom file will be loaded after this one*/
|
||||||
|
:root {
|
||||||
|
--bootstrap-theme: light;
|
||||||
|
|
||||||
|
--study-even-bg-color: rgb(250, 250, 250);
|
||||||
|
--study-odd-bg-color: rgb(240, 240, 240);
|
||||||
|
--study-hover-color: rgb(60, 180, 255);
|
||||||
|
--study-selected-color: rgb(235, 235, 235);
|
||||||
|
--study-details-bg-color: var(--study-selected-color);
|
||||||
|
--study-list-remote-bg-color: var(--study-hover-color);
|
||||||
|
|
||||||
|
--series-even-bg-color: rgb(240, 240, 240);
|
||||||
|
--series-odd-bg-color: rgb(235, 235, 235);
|
||||||
|
--series-hover-color: var(--study-hover-color);
|
||||||
|
--series-selected-color: rgb(225, 225, 225);
|
||||||
|
--series-details-bg-color: var(--series-selected-color);
|
||||||
|
|
||||||
|
--instance-odd-bg-color: rgb(230, 230, 230);
|
||||||
|
--instance-even-bg-color: rgb(225, 225, 225);
|
||||||
|
--instance-hover-color: var(--study-hover-color);
|
||||||
|
--instance-selected-color: rgb(215, 215, 215);
|
||||||
|
--instance-details-bg-color: var(--instance-selected-color);
|
||||||
|
|
||||||
|
--study-table-header-bg-color: var(--study-even-bg-color);
|
||||||
|
--study-table-filter-bg-color: rgb(220, 220, 220);
|
||||||
|
--study-table-actions-bg-color: var(--study-table-filter-bg-color);
|
||||||
|
|
||||||
|
--table-filters-is-searching-color: #fda90d86;
|
||||||
|
--table-filters-is-not-searching-color: rgb(30, 150, 255);
|
||||||
|
|
||||||
|
--details-top-margin: 25px;
|
||||||
|
|
||||||
|
--nav-bar-width: 260px;
|
||||||
|
|
||||||
|
--nav-side-color: rgb(255, 255, 255);
|
||||||
|
--nav-side-bg-color: #2e353d;
|
||||||
|
--nav-side-sub-bg-color: #181c20;
|
||||||
|
--nav-side-selected-bg-color: #4f5b69;
|
||||||
|
--nav-side-hover-bg-color: #3e454d;
|
||||||
|
--nav-side-submenu-color: rgb(255, 255, 255);
|
||||||
|
--nav-side-active-border-color: #d19b3d;
|
||||||
|
|
||||||
|
--label-bg-color: rgb(10, 200, 240);
|
||||||
|
|
||||||
|
--form-placeholder-color: #CCC;
|
||||||
|
|
||||||
|
/* you may also customize global bootstrap variables */
|
||||||
|
/* --bs-border-color: rgb(110, 160, 200); */
|
||||||
|
/* --bs-info-rgb: 255, 26, 35; */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* override the custom logo class */
|
||||||
|
/* .custom-logo {
|
||||||
|
height: 150px;
|
||||||
|
} */
|
||||||
|
|
||||||
|
|
||||||
|
/* datepicker variables */
|
||||||
|
/* .dp__theme_dark {
|
||||||
|
--dp-border-color: var(--bs-border-color);
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* optional, you may e.g. customize bootstrap buttons or any other class that is declared globally */
|
||||||
|
|
||||||
|
/*
|
||||||
|
.btn-primary {
|
||||||
|
--bs-btn-color: white;
|
||||||
|
--bs-btn-bg: red;
|
||||||
|
} */
|
||||||
3
WebApplication/src/assets/css/layout.css
Normal file
3
WebApplication/src/assets/css/layout.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.content {
|
||||||
|
margin-left: var(--nav-bar-width);
|
||||||
|
}
|
||||||
BIN
WebApplication/src/assets/images/orthanc.png
Normal file
BIN
WebApplication/src/assets/images/orthanc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
WebApplication/src/assets/logo.png
Normal file
BIN
WebApplication/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
335
WebApplication/src/components/AddSeriesModal.vue
Normal file
335
WebApplication/src/components/AddSeriesModal.vue
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
<script>
|
||||||
|
import { uppie } from "uppie"
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import resourceHelpers from "../helpers/resource-helpers"
|
||||||
|
import dateHelpers from "../helpers/date-helpers"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
import bootstrap from "bootstrap/dist/js/bootstrap.bundle.min.js"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["orthancStudyId", "studyMainDicomTags", "patientMainDicomTags"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
seriesTags: {}, // the series tag values entered in the dialog
|
||||||
|
seriesDateTags: {}, // same as tags but only for dates (for the DatePicker)
|
||||||
|
errorMessageId: null,
|
||||||
|
warningMessageId: null,
|
||||||
|
uploadedFileType: null,
|
||||||
|
uploadedFileDate: null,
|
||||||
|
uploadedFileBase64Content: null,
|
||||||
|
uploadedFileHumanSize: null,
|
||||||
|
step: 'prepare', // allowed values: 'prepare', 'error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.$refs['modal-main-div'].addEventListener('show.bs.modal', (e) => {
|
||||||
|
// move the modal to body to avoid z-index issues: https://weblog.west-wind.com/posts/2016/sep/14/bootstrap-modal-dialog-showing-under-modal-background
|
||||||
|
document.querySelector('body').appendChild(e.target);
|
||||||
|
this.reset();
|
||||||
|
});
|
||||||
|
uppie(document.querySelector("#addSeriesFileUpload-" + this.orthancStudyId), this.uppieUploadHandler);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async reset() {
|
||||||
|
this.step = 'prepare';
|
||||||
|
this.uploadedFileType = null;
|
||||||
|
this.uploadedFileDate = null;
|
||||||
|
this.uploadedFileHumanSize = null;
|
||||||
|
this.uploadedFileName = null;
|
||||||
|
this.uploadedFileBase64Content = null;
|
||||||
|
this.warningMessageId = null;
|
||||||
|
this.errorMessageId = null;
|
||||||
|
this.seriesTags = {};
|
||||||
|
document.getElementById("addSeriesFileUpload-" + this.orthancStudyId).value = null;
|
||||||
|
},
|
||||||
|
setError(errorMessageId) {
|
||||||
|
this.step = 'error';
|
||||||
|
this.errorMessageId = errorMessageId;
|
||||||
|
},
|
||||||
|
async addSeries() {
|
||||||
|
try {
|
||||||
|
let tags = {};
|
||||||
|
|
||||||
|
for (const [k,] of Object.entries(this.seriesTags)) {
|
||||||
|
if (dateHelpers.isDateTag(k)) {
|
||||||
|
tags[k] = dateHelpers.dicomDateFromDatePicker(this.seriesDateTags[k]);
|
||||||
|
} else {
|
||||||
|
tags[k] = this.seriesTags[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// if no AcquisitionDate and ContentDate, copy it from SeriesDate ...
|
||||||
|
if (!("AcquisitionDate" in tags)) {
|
||||||
|
tags["AcquisitionDate"] = tags["SeriesDate"]
|
||||||
|
}
|
||||||
|
if (!("ContentDate" in tags)) {
|
||||||
|
tags["ContentDate"] = tags["SeriesDate"]
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.createDicom(this.orthancStudyId, this.uploadedFileBase64Content, tags);
|
||||||
|
let closeButton = document.getElementById('add-series-close-'+ this.orthancStudyId);
|
||||||
|
closeButton.click();
|
||||||
|
this.messageBus.emit('added-series-to-study-' + this.orthancStudyId);
|
||||||
|
} catch (err) {
|
||||||
|
this.setError('add_series.error_add_series_unexpected_error_html');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isDateTag(tag) {
|
||||||
|
return dateHelpers.isDateTag(tag);
|
||||||
|
},
|
||||||
|
onDrop(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.analyzeFile(ev.dataTransfer.items[0].getAsFile());
|
||||||
|
},
|
||||||
|
onDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
getHumanSize(sizeInBytes) {
|
||||||
|
if (sizeInBytes > 1024*1024) {
|
||||||
|
return (Math.round(sizeInBytes/(1024*1024)*100) / 100) + " MB";
|
||||||
|
} else {
|
||||||
|
return (Math.round(sizeInBytes/1024*100) / 100) + " kB";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async uppieUploadHandler(event, formData, files) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.analyzeFile(event.target.files[0]);
|
||||||
|
},
|
||||||
|
async analyzeFile(file) {
|
||||||
|
console.log("analyzeFile", file);
|
||||||
|
this.uploadedFileDate = file.lastModifiedDate;
|
||||||
|
this.uploadedFileHumanSize = this.getHumanSize(file.size);
|
||||||
|
this.uploadedFileName = file.name;
|
||||||
|
this.warningMessageId = null;
|
||||||
|
|
||||||
|
if (file.type == 'application/pdf') {
|
||||||
|
this.uploadedFileType = "pdf";
|
||||||
|
} else if (file.type.startsWith('image/')) {
|
||||||
|
this.uploadedFileType = "image";
|
||||||
|
} else if (file.type == "" && file.name.endsWith ) {
|
||||||
|
this.uploadedFileType = "stl"
|
||||||
|
} else {
|
||||||
|
this.uploadedFileType = null;
|
||||||
|
this.warningMessageId = "add_series.unrecognized_file_format"
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(this.uiOptions.AddSeriesDefaultTags[this.uploadedFileType])) {
|
||||||
|
this.seriesTags[k] = v;
|
||||||
|
if (dateHelpers.isDateTag(k)) {
|
||||||
|
if (v == '$TODAY$') {
|
||||||
|
this.seriesDateTags[k] = new Date();
|
||||||
|
} else if (v == '$STUDY_DATE$') {
|
||||||
|
this.seriesDateTags[k] = dateHelpers.fromDicomDate(this.studyMainDicomTags["StudyDate"]);
|
||||||
|
} else if (v == '$FILE_DATE$') {
|
||||||
|
this.seriesDateTags[k] = this.uploadedFileDate;
|
||||||
|
} else {
|
||||||
|
this.seriesDateTags[k] = dateHelpers.fromDicomDate(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let reader = new FileReader();
|
||||||
|
let that = this;
|
||||||
|
reader.onload = function (event) {
|
||||||
|
that.uploadedFileBase64Content = event.target.result;
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions
|
||||||
|
}),
|
||||||
|
resourceTitle() {
|
||||||
|
return resourceHelpers.getResourceTitle('study', this.patientMainDicomTags, this.studyMainDicomTags, null, null);
|
||||||
|
},
|
||||||
|
addSeriesButtonEnabled() {
|
||||||
|
return this.uploadedFileType != null;
|
||||||
|
},
|
||||||
|
isDarkMode() {
|
||||||
|
// hack to switch the theme: get the value from our custom css
|
||||||
|
let bootstrapTheme = document.documentElement.getAttribute("data-bs-theme"); // for production
|
||||||
|
bootstrapTheme = getComputedStyle(document.documentElement).getPropertyValue('--bootstrap-theme'); // for dev
|
||||||
|
console.log("DatePicker color mode is ", bootstrapTheme);
|
||||||
|
return bootstrapTheme == "dark";
|
||||||
|
},
|
||||||
|
datePickerFormat() {
|
||||||
|
return this.uiOptions.DateFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
components: {}
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal fade" :id="this.orthancStudyId" tabindex="-1" aria-labelledby="modalLabel" ref="modal-main-div">
|
||||||
|
<!-- aria-hidden="true" -->
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="modalLabel">{{ $t("add_series.modal_title") + " " + resourceTitle }}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" :id="'add-series-close-'+ orthancStudyId"></button>
|
||||||
|
</div>
|
||||||
|
<!-- ------------------------------------------ step 'prepare' --------------------------------------------------->
|
||||||
|
<div v-if="step == 'prepare'" class="modal-body">
|
||||||
|
<div class="upload-drop-zone" @drop="this.onDrop" @dragover="this.onDragOver">
|
||||||
|
<div class="mb-3">{{ $t('add_series.drop_file') }}</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="btn btn-primary btn-file">
|
||||||
|
{{ $t('add_series.select_file') }} <input type="file" style="display: none;"
|
||||||
|
:id="'addSeriesFileUpload-' + this.orthancStudyId" required>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="warningMessageId != null" class="modal-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="alert alert-warning" role="alert" v-html="$t(warningMessageId)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="uploadedFileType != null" class="modal-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="alert alert-info" role="alert" v-html="$t('add_series.uploaded_file', {'size': uploadedFileHumanSize, 'type': uploadedFileType, 'name': uploadedFileName})">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="uploadedFileType != null" class="container" style="min-height: 40vh">
|
||||||
|
<!-- min height for the date picker-->
|
||||||
|
<div v-for="(item, key) in seriesTags" :key="key" class="row">
|
||||||
|
<!---- label ---->
|
||||||
|
<div class="col-md-5">
|
||||||
|
{{ key }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!---- edit text ---->
|
||||||
|
<div v-if="!isDateTag(key)" class="col-md-6">
|
||||||
|
<input v-if="true" type="text" class="form-control" v-model="seriesTags[key]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isDateTag(key)" class="col-md-6">
|
||||||
|
<Datepicker v-model="seriesDateTags[key]" :range="false" :enable-time-picker="false"
|
||||||
|
:format="datePickerFormat" :preview-format="datePickerFormat" text-input
|
||||||
|
arrow-navigation :highlight-week-days="[0, 6]" :dark="isDarkMode">
|
||||||
|
</Datepicker>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div v-if="step == 'prepare'" class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("cancel") }}</button>
|
||||||
|
<button type="button" class="btn btn-primary" :disabled="!addSeriesButtonEnabled"
|
||||||
|
@click="addSeries()">{{ $t('add_series.button_title') }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ------------------------------------------ step 'error' --------------------------------------------------->
|
||||||
|
<div v-if="step == 'error'" class="modal-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="alert alert-danger" role="alert" v-html="$t(errorMessageId)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="step == 'error'" class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click="back()">{{
|
||||||
|
$t("modify.back_button_title")
|
||||||
|
}}</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("cancel") }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* The switch - the box around the slider */
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide default HTML checkbox */
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The slider */
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked+.slider {
|
||||||
|
background-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus+.slider {
|
||||||
|
box-shadow: 0 0 1px #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked+.slider:before {
|
||||||
|
-webkit-transform: translateX(26px);
|
||||||
|
-ms-transform: translateX(26px);
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounded sliders */
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-text {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
line-height: var(--bs-body-line-height);
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-drop-zone {
|
||||||
|
border-color: white;
|
||||||
|
border-style: dashed;
|
||||||
|
border-width: 4px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
225
WebApplication/src/components/BulkLabelsModal.vue
Normal file
225
WebApplication/src/components/BulkLabelsModal.vue
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
<script>
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import api from "../orthancApi";
|
||||||
|
import Tags from "bootstrap5-tags/tags.js"
|
||||||
|
import bootstrap from "bootstrap/dist/js/bootstrap.bundle.min.js"
|
||||||
|
import LabelsEditor from "./LabelsEditor.vue";
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["resourcesOrthancId", "resourceLevel"],
|
||||||
|
emits:["bulkModalClosed"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
labelsToRemove: [],
|
||||||
|
labelsToAdd: [],
|
||||||
|
removedLabels: [],
|
||||||
|
messages: [],
|
||||||
|
addInProgress: false,
|
||||||
|
clearAllInProgress: false,
|
||||||
|
removeInProgress: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.reset();
|
||||||
|
// Tags.init();
|
||||||
|
|
||||||
|
this.$refs['modal-main-div'].addEventListener('show.bs.modal', (e) => {
|
||||||
|
// move the modal to body to avoid z-index issues: https://weblog.west-wind.com/posts/2016/sep/14/bootstrap-modal-dialog-showing-under-modal-background
|
||||||
|
document.querySelector('body').appendChild(e.target);
|
||||||
|
});
|
||||||
|
this.$refs['modal-main-div'].addEventListener('hide.bs.modal', (e) => {
|
||||||
|
this.$emit("bulkModalClosed");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async showModal() {
|
||||||
|
var myModal = new bootstrap.Modal(this.$el, {});
|
||||||
|
myModal.show();
|
||||||
|
},
|
||||||
|
async reset() {
|
||||||
|
this.labelsToRemove = [];
|
||||||
|
this.labelsToAdd = [];
|
||||||
|
this.removedLabels = [];
|
||||||
|
this.messages = [];
|
||||||
|
this.addInProgress = false;
|
||||||
|
this.clearAllInProgress = false;
|
||||||
|
this.removeInProgress = false;
|
||||||
|
},
|
||||||
|
async clearAllLabels() {
|
||||||
|
this.clearAllInProgress = true;
|
||||||
|
let promises = [];
|
||||||
|
|
||||||
|
for (const studyId of this.resourcesOrthancId) {
|
||||||
|
promises.push(await api.removeAllLabels(studyId));
|
||||||
|
}
|
||||||
|
const promisesResults = await Promise.all(promises);
|
||||||
|
let removedLabels = new Set();
|
||||||
|
for (const result of promisesResults) {
|
||||||
|
for (const label of result) {
|
||||||
|
removedLabels.add(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.messages.push({
|
||||||
|
labels: removedLabels,
|
||||||
|
part1: this.$t('labels.cleared_labels_message_part_1_html', {count: this.resourcesOrthancId.length}),
|
||||||
|
part2: this.$t('labels.cleared_labels_message_part_2_html', {count: this.resourcesOrthancId.length})
|
||||||
|
});
|
||||||
|
this.clearAllInProgress = false;
|
||||||
|
|
||||||
|
this.$store.dispatch('studies/refreshStudiesLabels', { studiesIds: this.resourcesOrthancId });
|
||||||
|
},
|
||||||
|
onLabelsToAddChanged(labelsToAdd) {
|
||||||
|
this.labelsToAdd = labelsToAdd;
|
||||||
|
},
|
||||||
|
onLabelsToRemoveChanged(labelsToRemove) {
|
||||||
|
this.labelsToRemove = labelsToRemove;
|
||||||
|
},
|
||||||
|
async addLabels() {
|
||||||
|
this.addInProgress = true;
|
||||||
|
let promises = [];
|
||||||
|
|
||||||
|
for (const label of this.labelsToAdd) {
|
||||||
|
for (const studyId of this.resourcesOrthancId) {
|
||||||
|
promises.push(api.addLabel({
|
||||||
|
studyId: studyId,
|
||||||
|
label: label
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const promisesResults = await Promise.all(promises);
|
||||||
|
let processedLabels = new Set();
|
||||||
|
for (const label of promisesResults) {
|
||||||
|
processedLabels.add(label);
|
||||||
|
}
|
||||||
|
this.messages.push({
|
||||||
|
labels: processedLabels,
|
||||||
|
part1: this.$t('labels.added_labels_message_part_1_html', {count: this.resourcesOrthancId.length}),
|
||||||
|
part2: this.$t('labels.added_labels_message_part_2_html', {count: this.resourcesOrthancId.length})
|
||||||
|
});
|
||||||
|
this.addInProgress = false;
|
||||||
|
|
||||||
|
this.$store.dispatch('studies/refreshStudiesLabels', { studiesIds: this.resourcesOrthancId });
|
||||||
|
},
|
||||||
|
isLabelToAdd(label) {
|
||||||
|
return this.labelsToAdd.includes(label);
|
||||||
|
},
|
||||||
|
isLabelToRemove(label) {
|
||||||
|
return this.labelsToRemove.includes(label);
|
||||||
|
},
|
||||||
|
async removeLabels() {
|
||||||
|
this.removeInProgress = true;
|
||||||
|
let promises = [];
|
||||||
|
|
||||||
|
for (const label of this.labelsToRemove) {
|
||||||
|
for (const studyId of this.resourcesOrthancId) {
|
||||||
|
promises.push(api.removeLabel({
|
||||||
|
studyId: studyId,
|
||||||
|
label: label
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const promisesResults = await Promise.all(promises);
|
||||||
|
let processedLabels = new Set();
|
||||||
|
for (const label of promisesResults) {
|
||||||
|
processedLabels.add(label);
|
||||||
|
}
|
||||||
|
this.messages.push({
|
||||||
|
labels: processedLabels,
|
||||||
|
part1: this.$t('labels.removed_labels_message_part_1_html', {count: this.resourcesOrthancId.length}),
|
||||||
|
part2: this.$t('labels.removed_labels_message_part_2_html', {count: this.resourcesOrthancId.length})
|
||||||
|
});
|
||||||
|
this.removeInProgress = false;
|
||||||
|
|
||||||
|
this.$store.dispatch('studies/refreshStudiesLabels', { studiesIds: this.resourcesOrthancId });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
allLabels: state => state.labels.allLabels
|
||||||
|
}),
|
||||||
|
isAddButtonEnabled() {
|
||||||
|
return !this.addInProgress && this.labelsToAdd.length > 0;
|
||||||
|
},
|
||||||
|
isRemoveButtonEnabled() {
|
||||||
|
return !this.removeInProgress && this.labelsToRemove.length > 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { LabelsEditor },
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal fade" id="bulk-labels-modal" tabindex="-1" aria-labelledby="modalLabel" ref="modal-main-div">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="modalLabel">{{ $t("labels.modal_title") }} </h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row border-bottom py-3">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<p v-html="$t('labels.clear_all_labels_message_html')"></p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="button" class="btn btn-primary w-100" @click="clearAllLabels()" :disabled="clearAllInProgress">
|
||||||
|
<span>{{ $t('labels.clear_all_button') }}</span>
|
||||||
|
<span v-if="clearAllInProgress" class="spinner-border spinner-border-sm alert-icon" role="status"
|
||||||
|
aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row border-bottom py-3">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<LabelsEditor id="addLabels" :labels="labelsToAdd" :title="'labels.add_labels_message_html'" :studyId="null" @labelsUpdated="onLabelsToAddChanged"></LabelsEditor>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="button" class="btn btn-primary w-100"
|
||||||
|
@click="addLabels()" :disabled="!isAddButtonEnabled">
|
||||||
|
<span>{{ $t('labels.add_button') }}</span>
|
||||||
|
<span v-if="addInProgress" class="spinner-border spinner-border-sm alert-icon" role="status"
|
||||||
|
aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row border-bottom py-3">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<LabelsEditor id="removeLabels" :labels="labelsToRemove" :title="'labels.remove_labels_message_html'" :studyId="null" @labelsUpdated="onLabelsToRemoveChanged"></LabelsEditor>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="button" class="btn btn-primary w-100"
|
||||||
|
@click="removeLabels()" :disabled="!isRemoveButtonEnabled">
|
||||||
|
<span>{{ $t('labels.remove_button') }}</span>
|
||||||
|
<span v-if="removeInProgress" class="spinner-border spinner-border-sm alert-icon" role="status"
|
||||||
|
aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row border-bottom py-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<p v-for="message in messages" :key="message">
|
||||||
|
<span v-html="message.part1"></span>
|
||||||
|
<span v-for="label in message.labels" :key="label" class="label badge">{{ label }}</span>
|
||||||
|
<span v-html="message.part2"></span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row py-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<button type="button" class="btn btn-primary w-100" data-bs-dismiss="modal" aria-label="Close">{{ $t('close') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
||||||
75
WebApplication/src/components/CopyToClipboardButton.vue
Normal file
75
WebApplication/src/components/CopyToClipboardButton.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<script>
|
||||||
|
import clipboardHelpers from "../helpers/clipboard-helpers"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["valueToCopy"],
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isSuccess: false,
|
||||||
|
isError: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSuccess() {
|
||||||
|
console.log('copied to clipboard: ' + this.valueToCopy);
|
||||||
|
this.isSuccess = true;
|
||||||
|
this.isError = false;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isSuccess = false;
|
||||||
|
}, 2500)
|
||||||
|
},
|
||||||
|
onFailure() {
|
||||||
|
console.log('failed to copy to clipboard');
|
||||||
|
this.isSuccess = false;
|
||||||
|
this.isError = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isError = false;
|
||||||
|
}, 2500)
|
||||||
|
},
|
||||||
|
click() {
|
||||||
|
clipboardHelpers.copyToClipboard(this.valueToCopy,
|
||||||
|
() => this.onSuccess(),
|
||||||
|
() => this.onFailure()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
},
|
||||||
|
components: {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button v-if="this.valueToCopy !== undefined" class="btn-clipboard"
|
||||||
|
:class="{ 'success': isSuccess, 'error': isError }" type="button" data-bs-toggle="tooltip"
|
||||||
|
title="Copy to clipboard" @click="click">
|
||||||
|
<i v-if="this.isSuccess" class="bi-check"></i>
|
||||||
|
<i v-if="this.isError" class="bi-bug"></i>
|
||||||
|
<i v-if="!this.isSuccess && !this.isError" class="bi-clipboard"></i>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-clipboard {
|
||||||
|
line-height: var(--bs-body-line-height);
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
127
WebApplication/src/components/InstanceDetails.vue
Normal file
127
WebApplication/src/components/InstanceDetails.vue
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<script>
|
||||||
|
import api from "../orthancApi";
|
||||||
|
import ResourceButtonGroup from "./ResourceButtonGroup.vue";
|
||||||
|
import TagsTree from "./TagsTree.vue";
|
||||||
|
import { mapState, mapGetters } from "vuex"
|
||||||
|
import SourceType from '../helpers/source-type';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['instanceId', 'seriesMainDicomTags', 'studyMainDicomTags', 'patientMainDicomTags'],
|
||||||
|
emits: ['deletedInstance'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tags: {},
|
||||||
|
headers: {},
|
||||||
|
loaded: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
studiesSourceType: state => state.studies.sourceType,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
if (this.studiesSourceType == SourceType.LOCAL_ORTHANC) {
|
||||||
|
this.tags = (await api.getInstanceTags(this.instanceId));
|
||||||
|
this.headers = (await api.getInstanceHeader(this.instanceId));
|
||||||
|
} else if (this.studiesSourceType == SourceType.REMOTE_DICOM) {
|
||||||
|
this.tags = {
|
||||||
|
"0008,0018" : {
|
||||||
|
"Name": "SOPInstanceUID",
|
||||||
|
"Type": "String",
|
||||||
|
"Value": this.instanceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.loaded = true;
|
||||||
|
},
|
||||||
|
components: { ResourceButtonGroup, TagsTree },
|
||||||
|
methods: {
|
||||||
|
onDeletedInstance() {
|
||||||
|
this.$emit('deletedInstance', this.instanceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table v-if="loaded" class="table table-responsive table-sm instance-details-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td width="80%" class="cut-text">
|
||||||
|
<TagsTree
|
||||||
|
:tags="headers">
|
||||||
|
</TagsTree>
|
||||||
|
</td>
|
||||||
|
<td width="20%" class="instance-button-group">
|
||||||
|
<ResourceButtonGroup
|
||||||
|
:resourceOrthancId="this.instanceId"
|
||||||
|
:resourceLevel="'instance'"
|
||||||
|
:studyMainDicomTags="this.studyMainDicomTags"
|
||||||
|
:seriesMainDicomTags="this.seriesMainDicomTags"
|
||||||
|
:patientMainDicomTags="this.patientMainDicomTags"
|
||||||
|
:instanceTags="this.tags"
|
||||||
|
:instanceHeaders="this.headers"
|
||||||
|
@deletedResource="onDeletedInstance"
|
||||||
|
></ResourceButtonGroup>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="80%" class="cut-text">
|
||||||
|
<TagsTree
|
||||||
|
:tags="tags">
|
||||||
|
</TagsTree>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.instance-details-table {
|
||||||
|
margin-top: var(--details-top-margin);
|
||||||
|
margin-left: 5%;
|
||||||
|
width: 95% !important;
|
||||||
|
background-color: var(--instance-details-bg-color) !important;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-details-table td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-details-table>:not(caption) >* >* {
|
||||||
|
background-color: var(--instance-details-bg-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-details-table >* >* {
|
||||||
|
background-color: var(--instance-details-bg-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.details-label {
|
||||||
|
font-weight: 700;
|
||||||
|
max-width: 30%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
margin-left: auto !important;
|
||||||
|
font-weight: 500;
|
||||||
|
max-width: 25vw;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.instance-button-group i {
|
||||||
|
font-size: 1.2rem !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
117
WebApplication/src/components/InstanceItem.vue
Normal file
117
WebApplication/src/components/InstanceItem.vue
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<script>
|
||||||
|
import InstanceDetails from "./InstanceDetails.vue";
|
||||||
|
import bootstrap from "bootstrap/dist/js/bootstrap.bundle.min.js"
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["instanceId", "instanceInfo", 'seriesMainDicomTags', 'studyMainDicomTags', 'patientMainDicomTags'],
|
||||||
|
emits: ['deletedInstance'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loaded: false,
|
||||||
|
expanded: false,
|
||||||
|
collapseElement: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
this.$refs['instance-collapsible-details'].addEventListener('show.bs.collapse', (e) => {
|
||||||
|
if (e.target == e.currentTarget) {
|
||||||
|
this.expanded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.$refs['instance-collapsible-details'].addEventListener('hide.bs.collapse', (e) => {
|
||||||
|
if (e.target == e.currentTarget) {
|
||||||
|
this.expanded = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var el = this.$refs['instance-collapsible-details'];
|
||||||
|
this.collapseElement = new bootstrap.Collapse(el, {toggle: false});
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(this.$route.query)) {
|
||||||
|
if (k === 'expand') {
|
||||||
|
if (v === 'instance') {
|
||||||
|
this.collapseElement.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDeletedInstance(instanceId) {
|
||||||
|
this.$emit('deletedInstance', instanceId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { InstanceDetails }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tbody>
|
||||||
|
<tr :class="{ 'instance-row-collapsed': !expanded, 'instance-row-expanded': expanded }">
|
||||||
|
<td></td>
|
||||||
|
<td
|
||||||
|
class="cut-text text-center"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
v-bind:data-bs-target="'#instance-details-' + this.instanceId"
|
||||||
|
>{{ instanceInfo.MainDicomTags.InstanceNumber }}</td>
|
||||||
|
<td
|
||||||
|
class="cut-text"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
v-bind:data-bs-target="'#instance-details-' + this.instanceId"
|
||||||
|
>{{ instanceInfo.MainDicomTags.SOPInstanceUID }}</td>
|
||||||
|
<td
|
||||||
|
class="cut-text text-center"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
v-bind:data-bs-target="'#instance-details-' + this.instanceId"
|
||||||
|
>{{ instanceInfo.MainDicomTags.NumberOfFrames }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
class="collapse"
|
||||||
|
:class="{ 'instance-details-collapsed': !expanded, 'instance-details-expanded': expanded }"
|
||||||
|
v-bind:id="'instance-details-' + this.instanceId"
|
||||||
|
ref="instance-collapsible-details"
|
||||||
|
>
|
||||||
|
<td colspan="100">
|
||||||
|
<InstanceDetails
|
||||||
|
v-if="this.expanded"
|
||||||
|
:instanceId="this.instanceId"
|
||||||
|
:studyMainDicomTags="this.studyMainDicomTags"
|
||||||
|
:seriesMainDicomTags="this.seriesMainDicomTags"
|
||||||
|
:patientMainDicomTags="this.patientMainDicomTags"
|
||||||
|
@deletedInstance="onDeletedInstance"
|
||||||
|
></InstanceDetails>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.instance-row-collapsed {
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-row-expanded {
|
||||||
|
background-color: var(--instance-details-bg-color);
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
border-top: 3px !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
border-color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-row-expanded>:first-child {
|
||||||
|
border-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.instance-details-expanded {
|
||||||
|
background-color: var(--instance-details-bg-color);
|
||||||
|
|
||||||
|
border-top: 0px !important;
|
||||||
|
border-bottom: 3px !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
border-color: black !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
109
WebApplication/src/components/InstanceList.vue
Normal file
109
WebApplication/src/components/InstanceList.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<script>
|
||||||
|
import InstanceItem from "./InstanceItem.vue"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['seriesId', 'seriesMainDicomTags', 'studyMainDicomTags', 'patientMainDicomTags', 'seriesInstances'],
|
||||||
|
emits: ['deletedInstance'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loaded: false,
|
||||||
|
instancesInfo: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sortedInstancesIds() {
|
||||||
|
if (this.loaded) {
|
||||||
|
let keys = Object.keys(this.instancesInfo);
|
||||||
|
keys.sort((a, b) => (parseInt(this.instancesInfo[a].IndexInSeries) > parseInt(this.instancesInfo[b].IndexInSeries) ? 1 : -1))
|
||||||
|
return keys;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
seriesInstances(newValue, oldValue) {
|
||||||
|
for (const instanceInfo of this.seriesInstances) {
|
||||||
|
this.instancesInfo[instanceInfo.ID] = instanceInfo;
|
||||||
|
}
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDeletedInstance(instanceId) {
|
||||||
|
delete this.instancesInfo[instanceId];
|
||||||
|
this.$emit("deletedInstance", instanceId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { InstanceItem }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table table-responsive table-sm instance-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="2%" scope="col" class="instance-table-header"></th>
|
||||||
|
<th width="7%" scope="col" class="instance-table-header cut-text" data-bs-toggle="tooltip"
|
||||||
|
:title="$t('dicom_tags.InstanceNumber')">{{ $t('dicom_tags.InstanceNumber') }}</th>
|
||||||
|
<th width="40%" scope="col" class="instance-table-header cut-text" data-bs-toggle="tooltip"
|
||||||
|
title="SOP Instance UID">SOP Instance UID</th>
|
||||||
|
<th width="5%" scope="col" class="series-table-header cut-text text-center" data-bs-toggle="tooltip"
|
||||||
|
:title="$t('dicom_tags.NumberOfFrames')"># {{$t('frames')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<InstanceItem v-for="instanceId in sortedInstancesIds" :key="instanceId" :instanceId="instanceId"
|
||||||
|
:instanceInfo="instancesInfo[instanceId]" :studyMainDicomTags="this.studyMainDicomTags"
|
||||||
|
:seriesMainDicomTags="this.seriesMainDicomTags" :patientMainDicomTags="this.patientMainDicomTags"
|
||||||
|
@deletedInstance="onDeletedInstance"></InstanceItem>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.instance-table {
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table> :not(:first-child) {
|
||||||
|
border-top: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table>:first-child {
|
||||||
|
border-bottom: 2px !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
border-color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table>:nth-child(odd) >* >* {
|
||||||
|
background-color: var(--instance-odd-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table>:nth-child(even) >* >* {
|
||||||
|
background-color: var(--instance-even-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table td {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table > tbody > tr:hover > * {
|
||||||
|
background-color: var(--instance-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table > tbody > tr.instance-row-expanded:hover > * {
|
||||||
|
background-color: var(--instance-details-bg-color);
|
||||||
|
}
|
||||||
|
.instance-table > tbody > tr.instance-details-expanded:hover > * {
|
||||||
|
background-color: var(--instance-details-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.instance-table-header {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
128
WebApplication/src/components/InstanceListExtended.vue
Normal file
128
WebApplication/src/components/InstanceListExtended.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<script>
|
||||||
|
import InstanceItem from "./InstanceItem.vue"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
import { ObserveVisibility as vObserveVisibility } from 'vue3-observe-visibility'
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['seriesId', 'seriesMainDicomTags', 'studyMainDicomTags', 'patientMainDicomTags', 'seriesInstances'],
|
||||||
|
emits: ['deletedInstance'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loaded: false,
|
||||||
|
instancesInfo: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sortedInstancesIds() {
|
||||||
|
if (this.loaded) {
|
||||||
|
let keys = Object.keys(this.instancesInfo);
|
||||||
|
keys.sort((a, b) => (parseInt(this.instancesInfo[a].IndexInSeries) > parseInt(this.instancesInfo[b].IndexInSeries) ? 1 : -1))
|
||||||
|
return keys;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
seriesInstances(newValue, oldValue) {
|
||||||
|
for (const instanceInfo of this.seriesInstances) {
|
||||||
|
this.instancesInfo[instanceInfo.ID] = instanceInfo;
|
||||||
|
}
|
||||||
|
this.loaded = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDeletedInstance(instanceId) {
|
||||||
|
delete this.instancesInfo[instanceId];
|
||||||
|
this.$emit("deletedInstance", instanceId);
|
||||||
|
},
|
||||||
|
async visibilityChanged(isVisible, entry) {
|
||||||
|
if (isVisible) {
|
||||||
|
let instanceId = entry.target.id;
|
||||||
|
// console.log(instanceId, this.seriesInstances);
|
||||||
|
if (instanceId == this.seriesInstances[this.seriesInstances.length - 1]['ID']) {
|
||||||
|
// console.log("Last element shown -> should load more instances");
|
||||||
|
const instances = await api.getSeriesInstancesExtended(this.seriesId, this.seriesInstances.length);
|
||||||
|
this.seriesInstances.push(...instances);
|
||||||
|
for (const instanceInfo of instances) {
|
||||||
|
this.instancesInfo[instanceInfo.ID] = instanceInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { InstanceItem }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table table-responsive table-sm instance-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="2%" scope="col" class="instance-table-header"></th>
|
||||||
|
<th width="7%" scope="col" class="instance-table-header cut-text" data-bs-toggle="tooltip"
|
||||||
|
:title="$t('dicom_tags.InstanceNumber')">{{ $t('dicom_tags.InstanceNumber') }}</th>
|
||||||
|
<th width="40%" scope="col" class="instance-table-header cut-text" data-bs-toggle="tooltip"
|
||||||
|
title="SOP Instance UID">SOP Instance UID</th>
|
||||||
|
<th width="5%" scope="col" class="series-table-header cut-text text-center" data-bs-toggle="tooltip"
|
||||||
|
:title="$t('dicom_tags.NumberOfFrames')"># {{$t('frames')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<InstanceItem v-for="instanceId in sortedInstancesIds" :key="instanceId" :id="instanceId" :instanceId="instanceId"
|
||||||
|
:instanceInfo="instancesInfo[instanceId]" :studyMainDicomTags="this.studyMainDicomTags"
|
||||||
|
:seriesMainDicomTags="this.seriesMainDicomTags" :patientMainDicomTags="this.patientMainDicomTags"
|
||||||
|
@deletedInstance="onDeletedInstance" v-observe-visibility="{callback: visibilityChanged, once: true}">
|
||||||
|
</InstanceItem>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.instance-table {
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table> :not(:first-child) {
|
||||||
|
border-top: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table>:first-child {
|
||||||
|
border-bottom: 2px !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
border-color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table>:nth-child(odd) >* >* {
|
||||||
|
background-color: var(--instance-odd-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table>:nth-child(even) >* >* {
|
||||||
|
background-color: var(--instance-even-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table td {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table > tbody > tr:hover > * {
|
||||||
|
background-color: var(--instance-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instance-table > tbody > tr.instance-row-expanded:hover > * {
|
||||||
|
background-color: var(--instance-details-bg-color);
|
||||||
|
}
|
||||||
|
.instance-table > tbody > tr.instance-details-expanded:hover > * {
|
||||||
|
background-color: var(--instance-details-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.instance-table-header {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
99
WebApplication/src/components/JobItem.vue
Normal file
99
WebApplication/src/components/JobItem.vue
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<script>
|
||||||
|
import Modal from "./Modal.vue"
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["job"],
|
||||||
|
emits: ["deletedJob"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isComplete: false,
|
||||||
|
isRunning: false,
|
||||||
|
isSuccess: false,
|
||||||
|
pctComplete: 0,
|
||||||
|
pctFailed: 0,
|
||||||
|
pctRemaining: 100,
|
||||||
|
refreshTimeout: 200,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
}),
|
||||||
|
statusText() {
|
||||||
|
if (this.isSuccess) {
|
||||||
|
return this.$t('job_success');
|
||||||
|
} else if (this.isComplete) {
|
||||||
|
return this.$t('job_failure');
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close(jobId) {
|
||||||
|
this.$emit("deletedJob", jobId);
|
||||||
|
},
|
||||||
|
async refresh(jobId) {
|
||||||
|
const jobStatus = await api.getJobStatus(jobId);
|
||||||
|
this.isComplete = (jobStatus.State == "Success" || jobStatus.State == "Failure");
|
||||||
|
this.isRunning = (jobStatus.State == "Running");
|
||||||
|
this.isSuccess = (jobStatus.State == "Success");
|
||||||
|
|
||||||
|
if (this.isComplete) {
|
||||||
|
|
||||||
|
this.pctRemaining = 0;
|
||||||
|
|
||||||
|
if (this.isSuccess) {
|
||||||
|
this.pctComplete = 100;
|
||||||
|
this.pctFailed = 0;
|
||||||
|
} else {
|
||||||
|
this.pctComplete = 0;
|
||||||
|
this.pctFailed = 100;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.pctFailed = 0;
|
||||||
|
this.pctComplete = jobStatus.Progress;
|
||||||
|
this.pctRemaining = 100 - this.pctComplete;
|
||||||
|
}
|
||||||
|
if (!this.isComplete) {
|
||||||
|
this.refreshTimeout = Math.min(this.refreshTimeout + 200, 2000); // refresh quickly at the beginnning !
|
||||||
|
setTimeout(this.refresh, this.refreshTimeout, [jobId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.refresh(this.job['id']);
|
||||||
|
},
|
||||||
|
components: { Modal }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card border-secondary job-card">
|
||||||
|
<div class="card-header jobs-header">
|
||||||
|
{{ job.name }}
|
||||||
|
<button type="button" class="btn-close job-card-close" aria-label="Close" @click="close(job.id)"></button>
|
||||||
|
<div class="progress mt-1 mb-1" style="width:90%">
|
||||||
|
<div class="progress-bar bg-success" role="progressbar"
|
||||||
|
v-bind:style="'width: ' + this.pctComplete + '%'">{{ statusText }}</div>
|
||||||
|
<div class="progress-bar bg-secondary" role="progressbar"
|
||||||
|
v-bind:style="'width: ' + this.pctRemaining + '%'"></div>
|
||||||
|
<div class="progress-bar bg-danger" role="progressbar"
|
||||||
|
v-bind:style="'width: ' + this.pctFailed + '%'">{{ statusText }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-secondary jobs-body">
|
||||||
|
<p class="card-text">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
46
WebApplication/src/components/JobsList.vue
Normal file
46
WebApplication/src/components/JobsList.vue
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<script>
|
||||||
|
import JobItem from "./JobItem.vue"
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
jobs: state => state.jobs.jobs,
|
||||||
|
jobsIds: state => state.jobs.jobsIds
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDeletedJobItem(jobId) {
|
||||||
|
this.$store.dispatch('jobs/removeJob', { jobId: jobId });
|
||||||
|
},
|
||||||
|
getJob(jobId) {
|
||||||
|
return this.jobs[jobId];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { JobItem }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="upload-report-list">
|
||||||
|
<JobItem
|
||||||
|
v-for="jobId of jobsIds"
|
||||||
|
:job="this.getJob(jobId)"
|
||||||
|
:key="jobId"
|
||||||
|
v-bind:href="jobId"
|
||||||
|
@deletedJob="onDeletedJobItem"
|
||||||
|
></JobItem>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
116
WebApplication/src/components/LabelsEditor.vue
Normal file
116
WebApplication/src/components/LabelsEditor.vue
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<script>
|
||||||
|
import { mapState, mapGetters } from "vuex"
|
||||||
|
import api from "../orthancApi";
|
||||||
|
import Tags from "bootstrap5-tags/tags.js"
|
||||||
|
import LabelHelpers from "../helpers/label-helpers"
|
||||||
|
|
||||||
|
|
||||||
|
window.filterLabel = (str) => {
|
||||||
|
return LabelHelpers.filterLabel(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['labels', 'studyId', 'id', 'title'],
|
||||||
|
emits: ["labelsUpdated"],
|
||||||
|
setup() {
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
labelsModel: [],
|
||||||
|
allLabelsLocalCopy: new Set()
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.labelsModel = this.labels;
|
||||||
|
if (this.canCreateNewLabels) {
|
||||||
|
// if we can create new labels, we already provide all the existing ones in the list
|
||||||
|
this.allLabelsLocalCopy = [...this.allLabels];
|
||||||
|
} else {
|
||||||
|
// if we can not create new labels, we only provide the available ones
|
||||||
|
this.allLabelsLocalCopy = [...this.uiOptions.AvailableLabels];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
Tags.init();
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
allLabels: state => state.labels.allLabels,
|
||||||
|
}),
|
||||||
|
getTitle() {
|
||||||
|
//console.log(this.title);
|
||||||
|
if (!this.title) {
|
||||||
|
return this.$t('labels.study_details_title');
|
||||||
|
}
|
||||||
|
return this.$t(this.title)
|
||||||
|
},
|
||||||
|
canCreateNewLabels() {
|
||||||
|
return this.uiOptions.AvailableLabels.length == 0; // if there is no AvailableLabels list, this means we can create any new label
|
||||||
|
},
|
||||||
|
placeHolderText() {
|
||||||
|
if (this.canCreateNewLabels) {
|
||||||
|
return this.$t('labels.add_labels_placeholder');
|
||||||
|
} else {
|
||||||
|
return this.$t('labels.add_labels_placeholder_no_create');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {},
|
||||||
|
methods: {
|
||||||
|
hasLabel(label) {
|
||||||
|
return this.labelsModel.includes(label);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
labelsModel: {
|
||||||
|
async handler(oldValue, newValue) {
|
||||||
|
if (this.studyId) {
|
||||||
|
let changed = await api.updateLabels({
|
||||||
|
studyId: this.studyId,
|
||||||
|
labels: this.labelsModel
|
||||||
|
});
|
||||||
|
if (changed) {
|
||||||
|
this.$emit("labelsUpdated");
|
||||||
|
setTimeout(() => { this.$store.dispatch('labels/refresh') }, 100);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.$emit("labelsUpdated", this.labelsModel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :id="id">
|
||||||
|
<label :for="'select-' + id" class="form-label"><span v-html="getTitle"></span><span
|
||||||
|
class="invalid-label-tips invalid-label-tips-hidden">{{ $t('labels.valid_alphabet_warning')
|
||||||
|
}}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select class="form-select" :id="'select-' + id" name="tags[]" v-model="labelsModel" multiple
|
||||||
|
data-allow-clear="true" :data-allow-new="canCreateNewLabels" data-badge-style="info" data-input-filter="filterLabel"
|
||||||
|
:placeholder="placeHolderText">
|
||||||
|
<option v-for="label in allLabelsLocalCopy" :key="label" :value="label" :selected="hasLabel(label)">{{ label
|
||||||
|
}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.invalid-label-tips {
|
||||||
|
color: red;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-label-tips-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
125
WebApplication/src/components/LanguagePicker.vue
Normal file
125
WebApplication/src/components/LanguagePicker.vue
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<script>
|
||||||
|
import allLanguages from '../store/modules/i18n';
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
props: [],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
availableLanguages: ['en'], // before the configuration is loaded, we consider that there is only Engligh
|
||||||
|
selectedLanguageName: ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
isConfigurationLoaded: state => state.configuration.loaded,
|
||||||
|
}),
|
||||||
|
showLanguagePicker() {
|
||||||
|
return this.availableLanguages.length > 1;
|
||||||
|
},
|
||||||
|
pickableLanguages() {
|
||||||
|
if (this.availableLanguages == null) {
|
||||||
|
return allLanguages;
|
||||||
|
} else {
|
||||||
|
let languages = [];
|
||||||
|
for (let languageCode of this.availableLanguages) {
|
||||||
|
for (let lang of allLanguages) {
|
||||||
|
if (lang['key'] == languageCode) {
|
||||||
|
languages.push(lang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return languages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isConfigurationLoaded(newValue, oldValue) {
|
||||||
|
this.availableLanguages = this.uiOptions.AvailableLanguages;
|
||||||
|
let done = false;
|
||||||
|
|
||||||
|
if (this.availableLanguages.length == 1) {
|
||||||
|
let languageKey = this.availableLanguages[0];
|
||||||
|
console.log("language picker: selecting first and only language: ", languageKey)
|
||||||
|
done = this.changeLanguage(languageKey);
|
||||||
|
} else if (this.availableLanguages.length > 1) {
|
||||||
|
|
||||||
|
if (localStorage.getItem("OE2.languageKey") != null) {
|
||||||
|
console.log("language picker: selecting language from the local storage");
|
||||||
|
done = this.changeLanguage(localStorage.getItem("OE2.languageKey"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!done && this.uiOptions.DefaultLanguage != null) {
|
||||||
|
console.log("language picker: selecting language from DefaultLanguage configuration");
|
||||||
|
done = this.changeLanguage(this.uiOptions.DefaultLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!done) {
|
||||||
|
console.log("language picker: selecting language from the browser languages");
|
||||||
|
if (navigator.languages && navigator.languages.length > 0) {
|
||||||
|
for (let lang of navigator.languages) {
|
||||||
|
let langCode = lang.split('-')[0];
|
||||||
|
done = this.changeLanguage(langCode);
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!done) {
|
||||||
|
done = this.changeLanguage(this.availableLanguages[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.selectedLanguageName = 'English'
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeLanguage(key) {
|
||||||
|
let languageName = null;
|
||||||
|
|
||||||
|
for (let lang of allLanguages) {
|
||||||
|
if (lang['key'] == key) {
|
||||||
|
languageName = lang['name'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (languageName != null) {
|
||||||
|
this.$i18n.locale = key;
|
||||||
|
this.selectedLanguageName = languageName;
|
||||||
|
|
||||||
|
localStorage.setItem("OE2.languageKey", key);
|
||||||
|
this.messageBus.emit('language-changed', key);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="dropdown" v-if="showLanguagePicker">
|
||||||
|
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton1" data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false">
|
||||||
|
<i class="bi bi-translate"></i> {{ selectedLanguageName }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu language-picker">
|
||||||
|
<li v-for="(lang, i) in pickableLanguages" :key="`lang-${i}`" :value="lang"><a class="dropdown-item" href="#"
|
||||||
|
@click="changeLanguage(`${lang.key}`)">{{ lang.name }}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.language-picker {
|
||||||
|
margin-top: var(--details-top-margin);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
--bs-dropdown-item-padding-y: 0.05rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
45
WebApplication/src/components/Modal.vue
Normal file
45
WebApplication/src/components/Modal.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["id", "headerText", "bodyText", "cancelText", "okText"],
|
||||||
|
methods: {
|
||||||
|
ok() {
|
||||||
|
this.$emit('ok');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$refs['modal-main-div'].addEventListener('show.bs.modal', (e) => {
|
||||||
|
// move the modal to body to avoid z-index issues: https://weblog.west-wind.com/posts/2016/sep/14/bootstrap-modal-dialog-showing-under-modal-background
|
||||||
|
document.querySelector('body').appendChild(e.target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal fade" :id="this.id" tabindex="-1" aria-labelledby="modalLabel" ref="modal-main-div">
|
||||||
|
<!-- aria-hidden="true" -->
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="modalLabel">{{ headerText }}</h5><!-- Can't use v-html here since the headerText can contain DICOM Tags including XSS code -->
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<slot name="modalBody">
|
||||||
|
<span v-html="bodyText"></span><!-- Safe to use v-html here since the bodyText only contains translated messages -->
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button v-if="cancelText" type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ cancelText
|
||||||
|
}}</button>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" @click="ok()">{{ okText }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
998
WebApplication/src/components/ModifyModal.vue
Normal file
998
WebApplication/src/components/ModifyModal.vue
Normal file
@ -0,0 +1,998 @@
|
|||||||
|
<script>
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import CopyToClipboardButton from "./CopyToClipboardButton.vue";
|
||||||
|
import resourceHelpers from "../helpers/resource-helpers"
|
||||||
|
import dateHelpers from "../helpers/date-helpers"
|
||||||
|
import clipboardHelpers from "../helpers/clipboard-helpers"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
import { v4 as uuidv4 } from "uuid"
|
||||||
|
|
||||||
|
// these tags can not be removed
|
||||||
|
document.requiredTags = [
|
||||||
|
'PatientID', 'PatientBirthDate', 'PatientSex', , 'PatientName',
|
||||||
|
'StudyInstanceUID', 'StudyDate', 'StudyTime', 'AccessionNumber', 'ReferringPhysicianName', 'StudyID',
|
||||||
|
'SeriesInstanceUID', 'Modality', 'ContrastBolusAgent', 'SeriesNumber', 'ImageOrientationPatient', 'SeriesType'
|
||||||
|
];
|
||||||
|
|
||||||
|
// list of actions:
|
||||||
|
// - 'attach-study-to-existing-patient',
|
||||||
|
// - 'modify-any-tags-in-one-study',
|
||||||
|
// - 'modify-patient-tags-in-all-studies',
|
||||||
|
// - 'anonymize-study',
|
||||||
|
// - 'attach-series-to-existing-study',
|
||||||
|
// - 'modify-series-tags',
|
||||||
|
// - 'create-new-study-from-series',
|
||||||
|
// - 'anonymize-series'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["orthancId", "studyMainDicomTags", "patientMainDicomTags", "seriesMainDicomTags", "isAnonymization", "resourceLevel"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tags: {}, // the tag values entered in the dialog
|
||||||
|
dateTags: {}, // same as tags but only for dates (for the DatePicker)
|
||||||
|
originalTags: {}, // the original tag values
|
||||||
|
removedTags: {},
|
||||||
|
insertedTags: new Set(),
|
||||||
|
samePatientStudiesCount: 0,
|
||||||
|
hasLoadedSamePatientsStudiesCount: false,
|
||||||
|
errorMessageId: null,
|
||||||
|
warningMessageId: null,
|
||||||
|
jobProgressComplete: 0,
|
||||||
|
jobProgressFailed: 0,
|
||||||
|
jobProgressRemaining: 100,
|
||||||
|
jobRefreshTimeout: 200,
|
||||||
|
jobIsComplete: false,
|
||||||
|
jobIsRunning: false,
|
||||||
|
jobIsSuccess: false,
|
||||||
|
showModifiedResourceKey: null,
|
||||||
|
showModifiedResourceValue: null,
|
||||||
|
modificationMode: null,
|
||||||
|
step: 'init', // allowed values: 'init', 'tags', 'warning', 'error', 'progress', 'done'
|
||||||
|
action: 'none'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.$refs['modal-main-div'].addEventListener('show.bs.modal', (e) => {
|
||||||
|
// move the modal to body to avoid z-index issues: https://weblog.west-wind.com/posts/2016/sep/14/bootstrap-modal-dialog-showing-under-modal-background
|
||||||
|
document.querySelector('body').appendChild(e.target);
|
||||||
|
this.reset();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setTagsFromDicomTags(mainDicomTags) {
|
||||||
|
for (const [k, v] of Object.entries(mainDicomTags)) {
|
||||||
|
this.tags[k] = v;
|
||||||
|
if (dateHelpers.isDateTag(k)) {
|
||||||
|
this.dateTags[k] = dateHelpers.fromDicomDate(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async reset() {
|
||||||
|
this.step = 'init';
|
||||||
|
if (!this.hasLoadedSamePatientsStudiesCount) {
|
||||||
|
// console.log("loading", this.hasLoadedSamePatientsStudiesCount);
|
||||||
|
this.samePatientStudiesCount = (await api.getSamePatientStudies(this.patientMainDicomTags, ['PatientID'])).length; // here, we only use the PatientID since this is the only tag used by Orthanc when modifying the resources
|
||||||
|
this.hasLoadedSamePatientsStudiesCount = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tags = {};
|
||||||
|
this.dateTags = {};
|
||||||
|
this.originalTags = {};
|
||||||
|
this.removedTags = {};
|
||||||
|
this.insertedTags = new Set();
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(this.patientMainDicomTags)) {
|
||||||
|
this.originalTags[k] = v;
|
||||||
|
this.removedTags[k] = false;
|
||||||
|
}
|
||||||
|
for (const [k, v] of Object.entries(this.studyMainDicomTags)) {
|
||||||
|
this.originalTags[k] = v;
|
||||||
|
this.removedTags[k] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.resourceLevel == "series") {
|
||||||
|
for (const [k, v] of Object.entries(this.seriesMainDicomTags)) {
|
||||||
|
this.originalTags[k] = v;
|
||||||
|
this.removedTags[k] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.jobProgressComplete = 0;
|
||||||
|
this.jobProgressFailed = 0;
|
||||||
|
this.jobProgressRemaining = 100;
|
||||||
|
|
||||||
|
if (this.resourceLevel == 'study') {
|
||||||
|
this.modificationMode = this.uiOptions.Modifications.DefaultMode;
|
||||||
|
} else {
|
||||||
|
this.modificationMode = this.uiOptions.Modifications.SeriesDefaultMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAnonymization) {
|
||||||
|
if (this.resourceLevel == 'study') {
|
||||||
|
this.goToNextStep('init', 'anonymize-study');
|
||||||
|
} else {
|
||||||
|
this.goToNextStep('init', 'anonymize-series');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
back() {
|
||||||
|
if (this.step == 'tags' && !this.isAnonymization) {
|
||||||
|
this.step = 'init';
|
||||||
|
} else if (this.step == 'warning' || this.step == 'error') {
|
||||||
|
this.step = 'tags';
|
||||||
|
} else {
|
||||||
|
console.error("unknown step for back function ", this.step);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setWarning(warningMessageId) {
|
||||||
|
this.warningMessageId = warningMessageId;
|
||||||
|
this.step = 'warning';
|
||||||
|
},
|
||||||
|
setError(errorMessageId) {
|
||||||
|
this.errorMessageId = errorMessageId;
|
||||||
|
this.step = 'error';
|
||||||
|
},
|
||||||
|
startMonitoringJob(jobId) {
|
||||||
|
this.step = 'progress';
|
||||||
|
this.jobRefreshTimeout = 200; // refresh quickly at the beginnning !
|
||||||
|
setTimeout(this.monitorJob, this.jobRefreshTimeout, [jobId]);
|
||||||
|
},
|
||||||
|
async monitorJob(jobId) {
|
||||||
|
const jobStatus = await api.getJobStatus(jobId);
|
||||||
|
this.jobIsComplete = (jobStatus.State == "Success" || jobStatus.State == "Failure");
|
||||||
|
this.jobIsRunning = (jobStatus.State == "Running");
|
||||||
|
this.jobIsSuccess = (jobStatus.State == "Success");
|
||||||
|
|
||||||
|
if (this.jobIsComplete) {
|
||||||
|
|
||||||
|
this.jobProgressRemaining = 0;
|
||||||
|
|
||||||
|
if (this.jobIsSuccess) {
|
||||||
|
this.jobProgressComplete = 100;
|
||||||
|
this.jobProgressFailed = 0;
|
||||||
|
} else {
|
||||||
|
this.jobProgressComplete = 0;
|
||||||
|
this.jobProgressFailed = 100;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.jobProgressFailed = 0;
|
||||||
|
this.jobProgressComplete = jobStatus.Progress;
|
||||||
|
this.jobProgressRemaining = 100 - this.jobProgressComplete;
|
||||||
|
}
|
||||||
|
if (!this.jobIsComplete) {
|
||||||
|
this.jobRefreshTimeout = Math.min(this.jobRefreshTimeout + 200, 2000); // refresh quickly at the beginnning !
|
||||||
|
setTimeout(this.monitorJob, this.jobRefreshTimeout, [jobId]);
|
||||||
|
} else {
|
||||||
|
this.step = 'done';
|
||||||
|
const jobType = jobStatus['Type'];
|
||||||
|
if (jobType == 'MergeStudy') {
|
||||||
|
const modifiedStudy = await api.getStudy(jobStatus['Content']['TargetStudy']);
|
||||||
|
this.showModifiedResourceValue = modifiedStudy['MainDicomTags']['StudyInstanceUID'];
|
||||||
|
this.showModifiedResourceKey = "StudyInstanceUID";
|
||||||
|
} else if (jobType == 'ResourceModification') {
|
||||||
|
if (jobStatus['Content']['Type'] == 'Patient') {
|
||||||
|
this.showModifiedResourceKey = "PatientID";
|
||||||
|
const modifiedPatient = await api.getPatient(jobStatus['Content']['ID']);
|
||||||
|
this.showModifiedResourceValue = modifiedPatient['MainDicomTags']['PatientID'];
|
||||||
|
} else if (jobStatus['Content']['Type'] == 'Study') {
|
||||||
|
this.showModifiedResourceKey = "StudyInstanceUID";
|
||||||
|
const modifiedStudy = await api.getStudy(jobStatus['Content']['ID']);
|
||||||
|
this.showModifiedResourceValue = modifiedStudy['MainDicomTags']['StudyInstanceUID'];
|
||||||
|
} else if (jobStatus['Content']['Type'] == 'Series') {
|
||||||
|
this.showModifiedResourceKey = "StudyInstanceUID";
|
||||||
|
const modifiedStudy = await api.getSeriesParentStudy(jobStatus['Content']['ID']);
|
||||||
|
this.showModifiedResourceValue = modifiedStudy['MainDicomTags']['StudyInstanceUID'];
|
||||||
|
} else {
|
||||||
|
console.error("not handled yet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async modify(hasAcceptedWarning = false) {
|
||||||
|
try {
|
||||||
|
// perform checks before modification
|
||||||
|
if (this.action == 'attach-study-to-existing-patient') {
|
||||||
|
// make sure we use an existing PatientID
|
||||||
|
const targetPatient = await api.findPatient(this.tags['PatientID']);
|
||||||
|
if (targetPatient) {
|
||||||
|
const jobId = await api.modifyResource({
|
||||||
|
resourceLevel: 'study',
|
||||||
|
orthancId: this.orthancId,
|
||||||
|
replaceTags: targetPatient.MainDicomTags,
|
||||||
|
keepTags: (this.keepDicomUids ? ['StudyInstanceUID', 'SeriesInstanceUID', 'SOPInstanceUID'] : []),
|
||||||
|
keepSource: this.keepSource,
|
||||||
|
removeTags: this.removedTagsList
|
||||||
|
});
|
||||||
|
console.log("attach-study-to-existing-patient: created job ", jobId);
|
||||||
|
this.startMonitoringJob(jobId);
|
||||||
|
} else {
|
||||||
|
console.error("Error while changing patient, the new PatientID does not exist ", this.tags['PatientID']);
|
||||||
|
this.setError('modify.error_attach_study_to_existing_patient_target_does_not_exist_html');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (this.action == 'modify-any-tags-in-one-study') {
|
||||||
|
|
||||||
|
if ('PatientID' in this.modifiedTags) {
|
||||||
|
console.log("modify-any-tags-in-one-study: PatientID has changed");
|
||||||
|
const targetPatient = await api.findPatient(this.tags['PatientID']);
|
||||||
|
if (targetPatient) {
|
||||||
|
console.error("modify-any-tags-in-one-study: Error while modifying patient-study tags, another patient with the same PatientID already exists ", this.tags['PatientID'], targetPatient);
|
||||||
|
this.setError('modify.error_modify_any_study_tags_patient_exists_html');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if ('PatientName' in this.modifiedTags || 'PatientBirthDate' in this.modifiedTags || 'PatientSex' in this.modifiedTags) {
|
||||||
|
console.log("modify-any-tags-in-one-study: Patient tags have changed");
|
||||||
|
this.samePatientStudiesCount = (await api.getSamePatientStudies(this.originalTags, ['PatientID'])).length; // here, we only use the PatientID since this is the only tag used by Orthanc when modifying the resources
|
||||||
|
if (this.samePatientStudiesCount > 1) {
|
||||||
|
console.error("modify-any-tags-in-one-study: Error while modifying patient-study tags, this patient has other studies, you can not modify patient tags ", this.originalTags['PatientID'], this.modifiedTags);
|
||||||
|
this.setError('modify.error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.log("modify-any-tags-in-one-study: this is the only patient study, we can modify all tags");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// at this point, it is safe to modify any tags
|
||||||
|
const jobId = await api.modifyResource({
|
||||||
|
resourceLevel: 'study',
|
||||||
|
orthancId: this.orthancId,
|
||||||
|
replaceTags: this.modifiedTags,
|
||||||
|
keepTags: (this.keepDicomUids ? ['StudyInstanceUID', 'SeriesInstanceUID', 'SOPInstanceUID'] : []),
|
||||||
|
keepSource: this.keepSource,
|
||||||
|
removeTags: this.removedTagsList
|
||||||
|
});
|
||||||
|
console.log("modify-any-tags-in-one-study: created job ", jobId);
|
||||||
|
this.startMonitoringJob(jobId);
|
||||||
|
|
||||||
|
} else if (this.action == 'modify-patient-tags-in-all-studies') {
|
||||||
|
// if we try to change the PatientID, make sure we do not reuse an existing PatientID
|
||||||
|
if (this.tags['PatientID'] != this.originalTags['PatientID']) {
|
||||||
|
const targetPatient = await api.findPatient(this.tags['PatientID']);
|
||||||
|
if (targetPatient) {
|
||||||
|
console.error("modify-patient-tags-in-all-studies: Error while modifying patient tags, another patient with the same PatientID already exists ", this.tags['PatientID'], targetPatient);
|
||||||
|
this.setError('modify.error_modify_patient_tags_another_patient_exists_with_same_patient_id_html');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalPatient = await api.findPatient(this.originalTags['PatientID']);
|
||||||
|
this.updateDateTags();
|
||||||
|
const jobId = await api.modifyResource({
|
||||||
|
resourceLevel: 'patient',
|
||||||
|
orthancId: originalPatient['ID'],
|
||||||
|
replaceTags: this.tags,
|
||||||
|
keepTags: (this.keepDicomUids ? ['StudyInstanceUID', 'SeriesInstanceUID', 'SOPInstanceUID'] : []),
|
||||||
|
removeTags: this.removedTagsList,
|
||||||
|
keepSource: this.keepSource
|
||||||
|
});
|
||||||
|
console.log("modify-patient-tags-in-all-studies: created job ", jobId);
|
||||||
|
this.startMonitoringJob(jobId);
|
||||||
|
} else if (this.action == 'anonymize-study') {
|
||||||
|
this.updateDateTags();
|
||||||
|
const jobId = await api.anonymizeResource({
|
||||||
|
resourceLevel: 'study',
|
||||||
|
orthancId: this.orthancId,
|
||||||
|
replaceTags: this.tags,
|
||||||
|
removeTags: this.removedTagsList
|
||||||
|
});
|
||||||
|
console.log("anonymize-study: created job ", jobId);
|
||||||
|
this.startMonitoringJob(jobId);
|
||||||
|
} else if (this.action == 'attach-series-to-existing-study') {
|
||||||
|
if (this.tags['StudyInstanceUID'] == this.originalTags['StudyInstanceUID']) {
|
||||||
|
console.error("attach-series-to-existing-study: Error target study is same as current study ", this.tags['StudyInstanceUID']);
|
||||||
|
this.setError('modify.error_attach_series_to_existing_study_target_unchanged_html');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const targetStudy = await api.findStudy(this.tags['StudyInstanceUID']);
|
||||||
|
if (targetStudy == null) {
|
||||||
|
console.error("attach-series-to-existing-study: target study not found ", this.tags['StudyInstanceUID']);
|
||||||
|
this.setError('modify.error_attach_series_to_existing_study_target_does_not_exist_html');
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const jobId = await api.mergeSeriesInExistingStudy({
|
||||||
|
seriesIds: [this.orthancId],
|
||||||
|
targetStudyId: targetStudy['ID'],
|
||||||
|
keepSource: false
|
||||||
|
});
|
||||||
|
console.log("attach-series-to-existing-study: created job ", jobId);
|
||||||
|
this.startMonitoringJob(jobId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.action == 'create-new-study-from-series') {
|
||||||
|
// check if a patient with the same PatientID already exists
|
||||||
|
const targetPatient = await api.findPatient(this.tags['PatientID']);
|
||||||
|
if (targetPatient) {
|
||||||
|
if (!hasAcceptedWarning) {
|
||||||
|
if (targetPatient['MainDicomTags']['PatientName'] != this.tags['PatientName']
|
||||||
|
|| targetPatient['MainDicomTags']['PatientBirthDate'] != this.tags['PatientBirthDate']
|
||||||
|
|| targetPatient['MainDicomTags']['PatientSex'] != this.tags['PatientSex']
|
||||||
|
) {
|
||||||
|
console.warn("create-new-study-from-series: Another patient exists but tags differ", targetPatient['MainDicomTags'], this.tags);
|
||||||
|
this.setWarning('modify.warning_create_new_study_from_series_html');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("create-new-study-from-series: warning accepted, copying existing Patient tags");
|
||||||
|
this.tags['PatientName'] = targetPatient['MainDicomTags']['PatientName'];
|
||||||
|
this.tags['PatientBirthDate'] = targetPatient['MainDicomTags']['PatientBirthDate'];
|
||||||
|
this.dateTags['PatientBirthDate'] = dateHelpers.fromDicomDate(targetPatient['MainDicomTags']['PatientBirthDate']);
|
||||||
|
this.tags['PatientSex'] = targetPatient['MainDicomTags']['PatientSex'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate a new StudyInstanceUID (since we perform modification at series level, orthanc would keep even if not listed in keepTags)
|
||||||
|
this.tags['StudyInstanceUID'] = await api.generateUid('study');
|
||||||
|
this.updateDateTags();
|
||||||
|
const jobId = await api.modifyResource({
|
||||||
|
resourceLevel: 'series',
|
||||||
|
orthancId: this.orthancId,
|
||||||
|
replaceTags: this.tags,
|
||||||
|
keepTags: ['SeriesInstanceUID', 'SOPInstanceUID'],
|
||||||
|
keepSource: false
|
||||||
|
});
|
||||||
|
console.log("create-new-study-from-series: created job ", jobId);
|
||||||
|
this.startMonitoringJob(jobId);
|
||||||
|
|
||||||
|
} else if (this.action == 'modify-series-tags') {
|
||||||
|
const jobId = await api.modifyResource({
|
||||||
|
resourceLevel: 'series',
|
||||||
|
orthancId: this.orthancId,
|
||||||
|
replaceTags: this.modifiedTags,
|
||||||
|
keepTags: (this.keepDicomUids ? ['SeriesInstanceUID', 'SOPInstanceUID'] : []),
|
||||||
|
keepSource: this.keepSource,
|
||||||
|
});
|
||||||
|
console.log("modify-series-tags: created job ", jobId);
|
||||||
|
this.startMonitoringJob(jobId);
|
||||||
|
} else if (this.action == 'anonymize-series') {
|
||||||
|
this.updateDateTags();
|
||||||
|
const jobId = await api.anonymizeResource({
|
||||||
|
resourceLevel: 'series',
|
||||||
|
orthancId: this.orthancId,
|
||||||
|
replaceTags: this.tags,
|
||||||
|
removeTags: this.removedTagsList
|
||||||
|
});
|
||||||
|
console.log("anonymize-study: created job ", jobId);
|
||||||
|
this.startMonitoringJob(jobId);
|
||||||
|
} else {
|
||||||
|
console.error('unhandled');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
this.setError('modify.error_modify_unexpected_error_html');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goToNextStep(step, action) {
|
||||||
|
if (step == 'init') {
|
||||||
|
this.step = 'tags';
|
||||||
|
this.action = action;
|
||||||
|
if (action == 'anonymize-study') {
|
||||||
|
const uuid = uuidv4();
|
||||||
|
|
||||||
|
this.tags = {};
|
||||||
|
this.dateTags = {};
|
||||||
|
this.tags['PatientID'] = uuid;
|
||||||
|
this.tags['PatientName'] = 'Anonymized ' + uuid.substr(0, 8);
|
||||||
|
this.tags['PatientBirthDate'] = '';
|
||||||
|
this.dateTags['PatientBirthDate'] = null;
|
||||||
|
this.tags['PatientSex'] = '';
|
||||||
|
if ('StudyDescription' in this.studyMainDicomTags) {
|
||||||
|
this.tags['StudyDescription'] = this.studyMainDicomTags['StudyDescription'];
|
||||||
|
}
|
||||||
|
} else if (action == 'attach-study-to-existing-patient') {
|
||||||
|
this.tags = {};
|
||||||
|
this.dateTags = {};
|
||||||
|
this.tags['PatientID'] = this.patientMainDicomTags['PatientID'];
|
||||||
|
} else if (action == 'modify-any-tags-in-one-study') {
|
||||||
|
this.tags = {};
|
||||||
|
this.dateTags = {};
|
||||||
|
this.setTagsFromDicomTags(this.patientMainDicomTags);
|
||||||
|
this.setTagsFromDicomTags(this.studyMainDicomTags);
|
||||||
|
} else if (action == 'modify-patient-tags-in-all-studies') {
|
||||||
|
this.tags = {};
|
||||||
|
this.dateTags = {};
|
||||||
|
this.setTagsFromDicomTags(this.patientMainDicomTags);
|
||||||
|
} else if (action == 'attach-series-to-existing-study') {
|
||||||
|
this.tags = {};
|
||||||
|
this.dateTags = {};
|
||||||
|
this.tags['StudyInstanceUID'] = "";
|
||||||
|
} else if (action == 'create-new-study-from-series') {
|
||||||
|
this.tags = {};
|
||||||
|
this.dateTags = {};
|
||||||
|
this.tags['PatientID'] = '';
|
||||||
|
this.tags['PatientName'] = '';
|
||||||
|
this.tags['PatientBirthDate'] = '';
|
||||||
|
this.dateTags['PatientBirthDate'] = null;
|
||||||
|
this.tags['PatientSex'] = '';
|
||||||
|
this.tags['StudyDescription'] = '';
|
||||||
|
} else if (action == 'modify-series-tags') {
|
||||||
|
this.tags = {};
|
||||||
|
this.dateTags = {};
|
||||||
|
this.setTagsFromDicomTags(this.seriesMainDicomTags);
|
||||||
|
} else if (action == 'anonymize-series') {
|
||||||
|
const uuid = uuidv4();
|
||||||
|
|
||||||
|
this.tags = {};
|
||||||
|
this.dateTags = {};
|
||||||
|
this.tags['PatientID'] = uuid;
|
||||||
|
this.tags['PatientName'] = 'Anonymized ' + uuid.substr(0, 8);
|
||||||
|
this.tags['PatientBirthDate'] = '';
|
||||||
|
this.dateTags['PatientBirthDate'] = null;
|
||||||
|
this.tags['PatientSex'] = '';
|
||||||
|
if ('StudyDescription' in this.studyMainDicomTags) {
|
||||||
|
this.tags['StudyDescription'] = this.studyMainDicomTags['StudyDescription'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('unknown action ', action);
|
||||||
|
}
|
||||||
|
} else if (this.step == 'warning') {
|
||||||
|
this.modify();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async showModifiedResources() {
|
||||||
|
let newUrl = "";
|
||||||
|
let query = {
|
||||||
|
'forceRefresh' : Date.now() // to force refresh
|
||||||
|
};
|
||||||
|
if (this.showModifiedResourceKey) {
|
||||||
|
query[this.showModifiedResourceKey] = this.showModifiedResourceValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$router.push({path: newUrl, query: query});
|
||||||
|
// location.reload();
|
||||||
|
},
|
||||||
|
isFrozenTag(tag) {
|
||||||
|
if (this.isRemovedTag(tag)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.isDicomUid(tag)) {
|
||||||
|
return this.keepDicomUids;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEditableTag(tag) {
|
||||||
|
if (this.isRemovedTag(tag)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.resourceLevel == 'series') {
|
||||||
|
if (this.action == 'attach-series-to-existing-study' && tag == 'StudyInstanceUID') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isDicomUid(tag)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
isDateTag(tag) {
|
||||||
|
return dateHelpers.isDateTag(tag);
|
||||||
|
},
|
||||||
|
isDicomUid(tag) {
|
||||||
|
return ['StudyInstanceUID', 'SeriesInstanceUID', 'SOPInstanceUID'].indexOf(tag) != -1;
|
||||||
|
},
|
||||||
|
isAutogeneratedDicomUid(tag) {
|
||||||
|
if (this.resourceLevel == 'series' && this.action == 'attach-series-to-existing-study') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.isDicomUid(tag)) {
|
||||||
|
return !this.keepDicomUids;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
isRemovable(tag) {
|
||||||
|
|
||||||
|
if (document.requiredTags.indexOf(tag) != -1) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isRemovedTag(tag) {
|
||||||
|
if (this.removedTags[tag] === undefined) {
|
||||||
|
console.warn("isRemovedTag: undefined tag");
|
||||||
|
}
|
||||||
|
return this.removedTags[tag];
|
||||||
|
},
|
||||||
|
toggleRemovedTag(tag) {
|
||||||
|
this.removedTags[tag] = !this.removedTags[tag];
|
||||||
|
if (this.removedTags[tag] && this.insertedTags.has(tag)) {
|
||||||
|
// the tag has been inserted, remove it completely
|
||||||
|
delete this.tags[tag];
|
||||||
|
delete this.dateTags[tag];
|
||||||
|
delete this.originalTags[tag];
|
||||||
|
delete this.removedTags[tag];
|
||||||
|
this.insertedTags.delete(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.removedTags) {
|
||||||
|
console.log(tag + " will be removed")
|
||||||
|
} else {
|
||||||
|
console.log(tag + " will not be removed")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
insertTag(ev, tag) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
console.log("insert tag " + tag, this.tags, this.removedTags);
|
||||||
|
this.tags[tag] = "";
|
||||||
|
if (dateHelpers.isDateTag(tag)) {
|
||||||
|
this.dateTags[tag] = null;
|
||||||
|
}
|
||||||
|
this.removedTags[tag] = false;
|
||||||
|
this.originalTags[tag] = null;
|
||||||
|
this.insertedTags.add(tag);
|
||||||
|
console.log("insert tag " + tag, this.tags, this.removedTags);
|
||||||
|
},
|
||||||
|
isModeAllowed(mode) {
|
||||||
|
if (this.resourceLevel == 'series' && this.uiOptions.Modifications.SeriesAllowedModes.indexOf(mode) != -1) {
|
||||||
|
return true;
|
||||||
|
} else if (this.resourceLevel == 'study' && this.uiOptions.Modifications.AllowedModes.indexOf(mode) != -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
canSelectModes() {
|
||||||
|
if (this.isAnonymization) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.resourceLevel == 'series' && this.uiOptions.Modifications.SeriesAllowedModes.length > 1) {
|
||||||
|
return this.action == 'modify-series-tags';
|
||||||
|
} else if (this.resourceLevel == 'study' && this.uiOptions.Modifications.AllowedModes.length > 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
updateDateTags() {
|
||||||
|
for (const [k,] of Object.entries(this.tags)) {
|
||||||
|
if (dateHelpers.isDateTag(k)) {
|
||||||
|
this.tags[k] = dateHelpers.dicomDateFromDatePicker(this.dateTags[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions
|
||||||
|
}),
|
||||||
|
resourceTitle() {
|
||||||
|
return resourceHelpers.getResourceTitle(this.resourceLevel, this.patientMainDicomTags, this.studyMainDicomTags, this.seriesMainDicomTags, null);
|
||||||
|
},
|
||||||
|
keepSource() {
|
||||||
|
if (this.modificationMode == "duplicate") {
|
||||||
|
return true;
|
||||||
|
} else if (this.modificationMode == "modify-new-uids") {
|
||||||
|
return false;
|
||||||
|
} else if (this.modificationMode == "modify-keep-uids") {
|
||||||
|
if (this.action == "attach-study-to-existing-patient") { // PatientID changes -> orthancID changes -> delete original
|
||||||
|
return false;
|
||||||
|
} else if (this.action == "modify-any-tags-in-one-study" || this.action == "modify-patient-tags-in-all-studies") {
|
||||||
|
if ('PatientID' in this.modifiedTags) {
|
||||||
|
return false; // PatientID changes -> orthancID changes -> delete original
|
||||||
|
} else {
|
||||||
|
return true; // PatientID does not change -> orthancID does not change -> do not delete original
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("unhandled");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("unhandled");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keepDicomUids() {
|
||||||
|
if (this.modificationMode == "modify-keep-uids") {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
areTagsModified() {
|
||||||
|
let areTagsRemoved = false;
|
||||||
|
for (const [, v] of Object.entries(this.removedTags)) {
|
||||||
|
if (v) {
|
||||||
|
areTagsRemoved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (Object.keys(this.modifiedTags).length > 0 || areTagsRemoved)
|
||||||
|
},
|
||||||
|
modifiedTags() {
|
||||||
|
this.updateDateTags();
|
||||||
|
|
||||||
|
let modifiedTags = {};
|
||||||
|
for (const [k,] of Object.entries(this.tags)) {
|
||||||
|
if (this.tags[k] != this.originalTags[k] && !this.removedTags[k]) {
|
||||||
|
modifiedTags[k] = this.tags[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modifiedTags;
|
||||||
|
},
|
||||||
|
insertableTags() {
|
||||||
|
let tags = [];
|
||||||
|
if (this.action == 'modify-any-tags-in-one-study') {
|
||||||
|
tags = tags.concat(this.uiOptions.StudyMainTags);
|
||||||
|
tags = tags.concat(this.uiOptions.PatientMainTags);
|
||||||
|
}
|
||||||
|
const that = this;
|
||||||
|
tags = tags.filter(function (value) {
|
||||||
|
return !(value in that.originalTags);
|
||||||
|
})
|
||||||
|
|
||||||
|
return tags.sort();
|
||||||
|
},
|
||||||
|
removedTagsList() {
|
||||||
|
let l = [];
|
||||||
|
for (const [k, v] of Object.entries(this.removedTags)) {
|
||||||
|
if (v) {
|
||||||
|
l.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return l;
|
||||||
|
},
|
||||||
|
modifyButtonTitle() {
|
||||||
|
if (this.isAnonymization) {
|
||||||
|
return 'modify.anonymize_button_title';
|
||||||
|
} else {
|
||||||
|
return 'modify.modify_button_title';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifyModeNewUidText() {
|
||||||
|
if (this.resourceLevel == 'series') {
|
||||||
|
return 'modify.modify_series_mode_new_uids_html'
|
||||||
|
} else {
|
||||||
|
return 'modify.modify_study_mode_new_uids_html'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifyModeKeepUidText() {
|
||||||
|
if (this.resourceLevel == 'series') {
|
||||||
|
return 'modify.modify_series_mode_keep_uids_html'
|
||||||
|
} else {
|
||||||
|
return 'modify.modify_study_mode_keep_uids_html'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifyModeDuplicateText() {
|
||||||
|
if (this.resourceLevel == 'series') {
|
||||||
|
return 'modify.modify_series_mode_duplicate_html'
|
||||||
|
} else {
|
||||||
|
return 'modify.modify_study_mode_duplicate_html'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifyButtonEnabled() {
|
||||||
|
if (this.isAnonymization) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if (this.action == 'modify-patient-tags-in-all-studies') { // specific use case for https://github.com/orthanc-server/orthanc-explorer-2/issues/37
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return this.areTagsModified;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isDarkMode() {
|
||||||
|
// hack to switch the theme: get the value from our custom css
|
||||||
|
let bootstrapTheme = document.documentElement.getAttribute("data-bs-theme"); // for production
|
||||||
|
bootstrapTheme = getComputedStyle(document.documentElement).getPropertyValue('--bootstrap-theme'); // for dev
|
||||||
|
console.log("DatePicker color mode is ", bootstrapTheme);
|
||||||
|
return bootstrapTheme == "dark";
|
||||||
|
},
|
||||||
|
datePickerFormat() {
|
||||||
|
return this.uiOptions.DateFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
components: { CopyToClipboardButton }
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal fade" :id="this.orthancId" tabindex="-1" aria-labelledby="modalLabel" ref="modal-main-div">
|
||||||
|
<!-- aria-hidden="true" -->
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 v-if="!isAnonymization" class="modal-title" id="modalLabel">{{ $t("modify.modify_modal_title") + " " + resourceTitle }} </h5>
|
||||||
|
<h5 v-if="isAnonymization" class="modal-title" id="modalLabel">{{ $t("modify.anonymize_modal_title") + " " + resourceTitle }} </h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ------------------------------------------ step 'init' --------------------------------------------------->
|
||||||
|
<div v-if="step == 'init'" class="modal-body">
|
||||||
|
<div class="container">
|
||||||
|
<div v-if="resourceLevel=='study' && !isAnonymization" class="row border-bottom pb-3">
|
||||||
|
<div class="col-md-9"
|
||||||
|
v-html="$t('modify.study_step_0_attach_study_to_existing_patient_html')">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="button" class="btn btn-primary w-100"
|
||||||
|
v-html="$t('modify.study_step_0_attach_study_to_existing_patient_button_title_html')"
|
||||||
|
@click="goToNextStep(step, 'attach-study-to-existing-patient')"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="resourceLevel=='study' && !isAnonymization && samePatientStudiesCount > 1" class="row border-bottom border-3 py-3">
|
||||||
|
<div class="col-md-9"
|
||||||
|
v-html="$t('modify.study_step_0_patient_has_other_studies_html', { count: samePatientStudiesCount })">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="button" class="btn btn-primary w-100"
|
||||||
|
v-html="$t('modify.study_step_0_patient_has_other_studies_button_title_html')"
|
||||||
|
@click="goToNextStep(step, 'modify-patient-tags-in-all-studies')"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="resourceLevel=='study' && !isAnonymization && samePatientStudiesCount >= 1" class="row pt-3">
|
||||||
|
<div class="col-md-9"
|
||||||
|
v-html="$t('modify.study_step_0_modify_study_html', { count: samePatientStudiesCount })">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="button" class="btn btn-primary w-100"
|
||||||
|
v-html="$t('modify.study_step_0_modify_study_button_title_html')"
|
||||||
|
@click="goToNextStep(step, 'modify-any-tags-in-one-study')"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="resourceLevel=='series' && !isAnonymization" class="row border-bottom pb-3">
|
||||||
|
<div class="col-md-9"
|
||||||
|
v-html="$t('modify.series_step_0_attach_series_to_existing_study_html')">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="button" class="btn btn-primary w-100"
|
||||||
|
v-html="$t('modify.series_step_0_attach_series_to_existing_study_button_title_html')"
|
||||||
|
@click="goToNextStep(step, 'attach-series-to-existing-study')"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="resourceLevel=='series' && !isAnonymization" class="row border-bottom border-3 py-3">
|
||||||
|
<div class="col-md-9"
|
||||||
|
v-html="$t('modify.series_step_0_create_new_study_html')">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="button" class="btn btn-primary w-100"
|
||||||
|
v-html="$t('modify.series_step_0_create_new_study_button_title_html')"
|
||||||
|
@click="goToNextStep(step, 'create-new-study-from-series')"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="resourceLevel=='series' && !isAnonymization" class="row pt-3">
|
||||||
|
<div class="col-md-9"
|
||||||
|
v-html="$t('modify.series_step_0_modify_series_html')">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button type="button" class="btn btn-primary w-100"
|
||||||
|
v-html="$t('modify.series_step_0_modify_series_button_title_html')"
|
||||||
|
@click="goToNextStep(step, 'modify-series-tags')"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="step == 'init'" class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("cancel") }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ------------------------------------------ step 'tags' --------------------------------------------------->
|
||||||
|
<div v-if="step == 'tags'" class="modal-body">
|
||||||
|
<div class="container" style="min-height: 50vh"> <!-- min height for the date picker-->
|
||||||
|
<div v-for="(item, key) in tags" :key="key" class="row">
|
||||||
|
<!---- label ---->
|
||||||
|
<div class="col-md-5" :class="{ 'striked-through': isRemovedTag(key) }">
|
||||||
|
{{ key }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!---- edit text ---->
|
||||||
|
<div v-if="isFrozenTag(key)" class="col-md-6">
|
||||||
|
<input v-if="true" type="text" class="form-control" disabled :class="{ 'striked-through': !isDicomUid(key) }"
|
||||||
|
v-model="originalTags[key]" />
|
||||||
|
</div>
|
||||||
|
<div v-if="isEditableTag(key) && !isDateTag(key)" class="col-md-6">
|
||||||
|
<input v-if="true" type="text" class="form-control" v-model="tags[key]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isEditableTag(key) && isDateTag(key)" class="col-md-6">
|
||||||
|
<Datepicker v-model="dateTags[key]" :range="false"
|
||||||
|
:enable-time-picker="false" :format="datePickerFormat"
|
||||||
|
:preview-format="datePickerFormat" text-input arrow-navigation :highlight-week-days="[0, 6]" :dark="isDarkMode">
|
||||||
|
</Datepicker>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isAutogeneratedDicomUid(key)" class="col-md-6">
|
||||||
|
<input v-if="true" type="text" class="form-control" disabled
|
||||||
|
:value="$t('modify.autogenerated_dicom_uid')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!---- buttons ---->
|
||||||
|
<div v-if="true" class="col-md-1">
|
||||||
|
<div class="d-flex flex-row-reverse">
|
||||||
|
<button v-if="isRemovable(key) && !isRemovedTag(key)" class="btn-small"
|
||||||
|
type="button" data-bs-toggle="tooltip" :title="$t('modify.remove_tag_tooltip')"
|
||||||
|
@click="toggleRemovedTag(key)">
|
||||||
|
<i v-if="!isRemovedTag(key)" class="bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
<button v-if="isRemovable(key) && isRemovedTag(key)" class="btn-small" type="button"
|
||||||
|
data-bs-toggle="tooltip" :title="$t('modify.remove_tag_undo_tooltip')"
|
||||||
|
@click="toggleRemovedTag(key)">
|
||||||
|
<i v-if="isRemovedTag(key)" class="bi-recycle"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="action == 'modify-any-tags-in-one-study'" class="row pt-3">
|
||||||
|
<div class="col-md-8"></div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button"
|
||||||
|
id="dropdownInsertTag" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
{{ $t('modify.insert_tag') }}
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li v-for="key of insertableTags" :key="key" :value="key"><a
|
||||||
|
class="dropdown-item" href="#" @click="insertTag($event, key)">{{ key }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="canSelectModes()" class="row pt-3">
|
||||||
|
<div v-if="isModeAllowed('modify-new-uids')" class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="modificationMode" id="modifyNewUids"
|
||||||
|
value="modify-new-uids" v-model="modificationMode">
|
||||||
|
<label class="form-check-label" for="modifyNewUids"
|
||||||
|
v-html="$t(modifyModeNewUidText)">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="isModeAllowed('modify-keep-uids')" class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="modificationMode" id="modifyKeepUids"
|
||||||
|
value="modify-keep-uids" v-model="modificationMode">
|
||||||
|
<label class="form-check-label" for="modifyKeepUids"
|
||||||
|
v-html="$t(modifyModeKeepUidText)">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="isModeAllowed('duplicate')" class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="modificationMode"
|
||||||
|
id="modifyDuplicate" value="duplicate" v-model="modificationMode">
|
||||||
|
<label class="form-check-label" for="modifyDuplicate"
|
||||||
|
v-html="$t(modifyModeDuplicateText)">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="step == 'tags'" class="modal-footer">
|
||||||
|
<button v-if="!isAnonymization" type="button" class="btn btn-secondary" @click="back()">{{
|
||||||
|
$t("modify.back_button_title")
|
||||||
|
}}</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("cancel") }}</button>
|
||||||
|
<button type="button" class="btn btn-primary" :disabled="!modifyButtonEnabled" @click="modify()">{{
|
||||||
|
$t(modifyButtonTitle)
|
||||||
|
}}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ------------------------------------------ step 'warning' or 'error' --------------------------------------------------->
|
||||||
|
<div v-if="step == 'warning' || step == 'error'" class="modal-body">
|
||||||
|
<div class="container">
|
||||||
|
<div v-if="step == 'error'" class="alert alert-danger" role="alert" v-html="$t(errorMessageId)">
|
||||||
|
</div>
|
||||||
|
<div v-if="step == 'warning'" class="alert alert-warning" role="alert"
|
||||||
|
v-html="$t(warningMessageId)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="step == 'warning'" class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click="back()">{{
|
||||||
|
$t("modify.back_button_title")
|
||||||
|
}}</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("cancel") }}</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="modify(true)">{{
|
||||||
|
$t(modifyButtonTitle)
|
||||||
|
}}</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="step == 'error'" class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @click="back()">{{
|
||||||
|
$t("modify.back_button_title")
|
||||||
|
}}</button>
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("cancel") }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ------------------------------------------ step 'progress' and 'done' --------------------------------------------------->
|
||||||
|
|
||||||
|
<div v-if="step == 'progress' || step == 'done'" class="modal-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row pb-3">{{ $t('modify.job_in_progress') }}</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="progress" style="width:100%">
|
||||||
|
<div class="progress-bar bg-success" role="progressbar"
|
||||||
|
v-bind:style="'width: ' + this.jobProgressComplete + '%'"></div>
|
||||||
|
<div class="progress-bar bg-secondary" role="progressbar"
|
||||||
|
v-bind:style="'width: ' + this.jobProgressRemaining + '%'"></div>
|
||||||
|
<div class="progress-bar bg-danger" role="progressbar"
|
||||||
|
v-bind:style="'width: ' + this.jobProgressFailed + '%'"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="step == 'progress' || step == 'done'" class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ $t("close") }}</button>
|
||||||
|
<button type="button" class="btn btn-primary" :disabled="step != 'done'" data-bs-dismiss="modal"
|
||||||
|
@click="showModifiedResources()">{{ $t("modify.show_modified_resources") }}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* The switch - the box around the slider */
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide default HTML checkbox */
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The slider */
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked+.slider {
|
||||||
|
background-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus+.slider {
|
||||||
|
box-shadow: 0 0 1px #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked+.slider:before {
|
||||||
|
-webkit-transform: translateX(26px);
|
||||||
|
-ms-transform: translateX(26px);
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounded sliders */
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-text {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
line-height: var(--bs-body-line-height);
|
||||||
|
border: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.striked-through {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
22
WebApplication/src/components/NotFound.vue
Normal file
22
WebApplication/src/components/NotFound.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: [],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
path: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.path = this.$router.currentRoute.value.fullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
{{$t('page_not_found')}} <br/>
|
||||||
|
{{path}}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
1006
WebApplication/src/components/ResourceButtonGroup.vue
Normal file
1006
WebApplication/src/components/ResourceButtonGroup.vue
Normal file
File diff suppressed because it is too large
Load Diff
78
WebApplication/src/components/ResourceDetailText.vue
Normal file
78
WebApplication/src/components/ResourceDetailText.vue
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<script>
|
||||||
|
import CopyToClipboardButton from "./CopyToClipboardButton.vue";
|
||||||
|
import { translateDicomTag } from "../locales/i18n"
|
||||||
|
import dateHelpers from "../helpers/date-helpers"
|
||||||
|
import { mapState, mapGetters } from "vuex"
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["truncate", "copy", "tags", "tag", "showIfEmpty"], // you can set copy=false if you don't wan't the copy button !
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
}),
|
||||||
|
title() {
|
||||||
|
return translateDicomTag(this.$i18n.t, this.$i18n.te, this.tag);
|
||||||
|
},
|
||||||
|
value() {
|
||||||
|
if (this.hasValue) {
|
||||||
|
let value = this.tags[this.tag];
|
||||||
|
if (dateHelpers.isDateTag(this.tag)) {
|
||||||
|
return dateHelpers.formatDateForDisplay(value, this.uiOptions.DateFormat)
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasValue() {
|
||||||
|
return this.tag in this.tags;
|
||||||
|
},
|
||||||
|
showValue() {
|
||||||
|
return this.showIfEmpty || this.hasValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { CopyToClipboardButton }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<li v-if="showValue" class="d-flex">
|
||||||
|
<span class="details-label" :class="{ 'd-inline-block text-truncate': this.truncate }" data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="title">{{ title }}: </span>
|
||||||
|
<span class="details" data-bs-toggle="tooltip" v-bind:title="value">{{ value }}</span>
|
||||||
|
<CopyToClipboardButton v-if="!this.copy && value && value.length > 0" :valueToCopy="value" />
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.details-label {
|
||||||
|
font-weight: 700;
|
||||||
|
max-width: 30%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
margin-left: auto !important;
|
||||||
|
font-weight: 500;
|
||||||
|
max-width: 25vw;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
151
WebApplication/src/components/RetrieveAndView.vue
Normal file
151
WebApplication/src/components/RetrieveAndView.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<script>
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [],
|
||||||
|
async created() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
if (params.has("StudyInstanceUID")) {
|
||||||
|
this.studyInstanceUid = params.get('StudyInstanceUID');
|
||||||
|
} else if (params.has("study")) {
|
||||||
|
this.studyInstanceUid = params.get('study');
|
||||||
|
} else {
|
||||||
|
console.error("No study defined. Use StudyInstanceUID=1.2.3.... in your url");
|
||||||
|
}
|
||||||
|
|
||||||
|
const modality = params.get('modality');
|
||||||
|
if (params.has("viewer")) {
|
||||||
|
this.viewer = params.get("viewer");
|
||||||
|
}
|
||||||
|
|
||||||
|
const study = await api.findStudy( this.studyInstanceUid);
|
||||||
|
if (study == null) {
|
||||||
|
this.state = "finding-remotely";
|
||||||
|
|
||||||
|
const remoteStudies = await api.remoteDicomFind("Study", modality, {"StudyInstanceUID": this.studyInstanceUid}, true /* isUnique */);
|
||||||
|
if (remoteStudies.length == 0) {
|
||||||
|
this.state = "not-found";
|
||||||
|
} else {
|
||||||
|
if (this.remoteMode == 'dicom') {
|
||||||
|
const moveQuery = {
|
||||||
|
"StudyInstanceUID": this.studyInstanceUid
|
||||||
|
};
|
||||||
|
|
||||||
|
const jobId = await api.remoteDicomRetrieveResource("Study", modality, moveQuery, this.system.DicomAet);
|
||||||
|
this.state = "retrieving";
|
||||||
|
this.startMonitoringJob(jobId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.studyOrthancId = study['ID'];
|
||||||
|
this.openViewer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
state: "finding-locally",
|
||||||
|
remoteMode: "dicom",
|
||||||
|
viewer: "stone-viewer",
|
||||||
|
retrievedInstancesCount: 0,
|
||||||
|
studyInstanceUid: null,
|
||||||
|
studyOrthancId: null,
|
||||||
|
jobProgressComplete: 0,
|
||||||
|
jobProgressFailed: 0,
|
||||||
|
jobProgressRemaining: 100,
|
||||||
|
jobRefreshTimeout: 200,
|
||||||
|
jobIsComplete: false,
|
||||||
|
jobIsRunning: false,
|
||||||
|
jobIsSuccess: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
startMonitoringJob(jobId) {
|
||||||
|
this.jobRefreshTimeout = 200; // refresh quickly at the beginnning !
|
||||||
|
setTimeout(this.monitorJob, this.jobRefreshTimeout, [jobId]);
|
||||||
|
},
|
||||||
|
openViewer() {
|
||||||
|
if (this.viewer == "stone-viewer") {
|
||||||
|
window.location.href = api.getStoneViewerUrl("study", this.studyInstanceUid);
|
||||||
|
} else if (this.viewer == "osimis-viewer") {
|
||||||
|
window.location.href = api.getOsimisViewerUrl("study", this.studyOrthancId);
|
||||||
|
} else if (this.viewer == "ohif-viewer") {
|
||||||
|
window.location.href = api.getOh("study", this.studyOrthancId);
|
||||||
|
} else {
|
||||||
|
console.error("unsupported viewer: ", this.viewer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async monitorJob(jobId) {
|
||||||
|
const jobStatus = await api.getJobStatus(jobId);
|
||||||
|
this.jobIsComplete = (jobStatus.State == "Success" || jobStatus.State == "Failure");
|
||||||
|
this.jobIsRunning = (jobStatus.State == "Running");
|
||||||
|
this.jobIsSuccess = (jobStatus.State == "Success");
|
||||||
|
|
||||||
|
if (this.jobIsComplete) {
|
||||||
|
|
||||||
|
this.jobProgressRemaining = 0;
|
||||||
|
|
||||||
|
if (this.jobIsSuccess) {
|
||||||
|
this.jobProgressComplete = 100;
|
||||||
|
this.jobProgressFailed = 0;
|
||||||
|
} else {
|
||||||
|
this.jobProgressComplete = 0;
|
||||||
|
this.jobProgressFailed = 100;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.studyOrthancId == null) {
|
||||||
|
const study = await api.findStudy(this.studyInstanceUid);
|
||||||
|
if (study != null) {
|
||||||
|
this.studyOrthancId = study['ID'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const instances = await api.getStudyInstances(this.studyOrthancId);
|
||||||
|
this.retrievedInstancesCount = instances.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.jobProgressFailed = 0;
|
||||||
|
this.jobProgressComplete = jobStatus.Progress;
|
||||||
|
this.jobProgressRemaining = 100 - this.jobProgressComplete;
|
||||||
|
}
|
||||||
|
if (!this.jobIsComplete) {
|
||||||
|
this.jobRefreshTimeout = Math.min(this.jobRefreshTimeout + 200, 2000); // refresh quickly at the beginnning !
|
||||||
|
setTimeout(this.monitorJob, this.jobRefreshTimeout, [jobId]);
|
||||||
|
} else {
|
||||||
|
this.openViewer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
system: state => state.configuration.system,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
components: {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="d-flex flex-column min-vh-100 justify-content-center align-items-center h4 text-center">
|
||||||
|
<span>
|
||||||
|
<p v-if="state=='finding-locally'" v-html="$t('retrieve_and_view.finding_locally')"></p>
|
||||||
|
<p v-if="state=='finding-remotely'" v-html="$t('retrieve_and_view.finding_remotely')"></p>
|
||||||
|
<p v-if="state=='not-found'" v-html="$t('retrieve_and_view.not_found')"></p>
|
||||||
|
<p v-if="state=='retrieving'" v-html="$t('retrieve_and_view.retrieving')"></p>
|
||||||
|
<p v-if="state=='retrieving'" v-html="$t('retrieve_and_view.retrieved_html', { count: retrievedInstancesCount })"></p>
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
165
WebApplication/src/components/SeriesDetails.vue
Normal file
165
WebApplication/src/components/SeriesDetails.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<script>
|
||||||
|
import ResourceButtonGroup from "./ResourceButtonGroup.vue";
|
||||||
|
import InstanceList from "./InstanceList.vue";
|
||||||
|
import InstanceListExtended from "./InstanceListExtended.vue";
|
||||||
|
import ResourceDetailText from "./ResourceDetailText.vue";
|
||||||
|
import { mapState, mapGetters } from "vuex"
|
||||||
|
import api from "../orthancApi";
|
||||||
|
import SourceType from '../helpers/source-type';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['seriesId', 'seriesMainDicomTags', 'studyMainDicomTags', 'patientMainDicomTags', 'instancesIds'],
|
||||||
|
emits: ['deletedSeries'],
|
||||||
|
setup() {
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
seriesInstances: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
studiesSourceType: state => state.studies.sourceType,
|
||||||
|
studiesRemoteSource: state => state.studies.remoteSource,
|
||||||
|
hasExtendedFind: state => state.configuration.hasExtendedFind
|
||||||
|
}),
|
||||||
|
useExtendedInstanceList() {
|
||||||
|
return this.hasExtendedFind && this.studiesSourceType == SourceType.LOCAL_ORTHANC;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
if (this.studiesSourceType == SourceType.LOCAL_ORTHANC) {
|
||||||
|
if (this.useExtendedInstanceList) {
|
||||||
|
this.seriesInstances = await api.getSeriesInstancesExtended(this.seriesId, null);
|
||||||
|
} else {
|
||||||
|
this.seriesInstances = await api.getSeriesInstances(this.seriesId);
|
||||||
|
}
|
||||||
|
} else if (this.studiesSourceType == SourceType.REMOTE_DICOM) {
|
||||||
|
let remoteInstances = (await api.remoteDicomFind("Instance", this.studiesRemoteSource, {
|
||||||
|
"StudyInstanceUID": this.studyMainDicomTags.StudyInstanceUID,
|
||||||
|
"PatientID": this.patientMainDicomTags.PatientID,
|
||||||
|
"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);
|
||||||
|
} 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, InstanceListExtended, ResourceDetailText },
|
||||||
|
methods: {
|
||||||
|
onDeletedInstance(instanceId) {
|
||||||
|
const pos = this.instancesIds.indexOf(instanceId);
|
||||||
|
if (pos >= 0) {
|
||||||
|
this.instancesIds.splice(pos, 1);
|
||||||
|
if (this.instancesIds.length == 0) {
|
||||||
|
this.$emit("deletedSeries", this.seriesId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDeletedSeries() {
|
||||||
|
this.$emit("deletedSeries", this.seriesId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table table-responsive table-sm series-details-table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td width="70%" class="cut-text">
|
||||||
|
<ul>
|
||||||
|
<ResourceDetailText v-for="tag in uiOptions.SeriesMainTags" :key="tag" :tags="seriesMainDicomTags" :tag="tag" :showIfEmpty="true"></ResourceDetailText>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td width="30%" class="series-button-group">
|
||||||
|
<ResourceButtonGroup
|
||||||
|
:resourceOrthancId="this.seriesId"
|
||||||
|
:resourceLevel="'series'"
|
||||||
|
:resourceDicomUid="this.seriesMainDicomTags.SeriesInstanceUID"
|
||||||
|
:studyMainDicomTags="this.studyMainDicomTags"
|
||||||
|
:seriesMainDicomTags="this.seriesMainDicomTags"
|
||||||
|
:patientMainDicomTags="this.patientMainDicomTags"
|
||||||
|
:seriesInstances="this.seriesInstances"
|
||||||
|
:customClass="'instance-button-group'"
|
||||||
|
@deletedResource="onDeletedSeries"
|
||||||
|
></ResourceButtonGroup>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="100">
|
||||||
|
<InstanceList v-if="!useExtendedInstanceList"
|
||||||
|
:seriesId="this.seriesId"
|
||||||
|
:seriesMainDicomTags="this.seriesMainDicomTags"
|
||||||
|
:patientMainDicomTags="this.patientMainDicomTags"
|
||||||
|
:studyMainDicomTags="this.studyMainDicomTags"
|
||||||
|
:instancesIds="this.instancesIds"
|
||||||
|
:seriesInstances="this.seriesInstances"
|
||||||
|
@deletedInstance="onDeletedInstance"
|
||||||
|
></InstanceList>
|
||||||
|
<InstanceListExtended v-if="useExtendedInstanceList"
|
||||||
|
:seriesId="this.seriesId"
|
||||||
|
:seriesMainDicomTags="this.seriesMainDicomTags"
|
||||||
|
:patientMainDicomTags="this.patientMainDicomTags"
|
||||||
|
:studyMainDicomTags="this.studyMainDicomTags"
|
||||||
|
:instancesIds="this.instancesIds"
|
||||||
|
:seriesInstances="this.seriesInstances"
|
||||||
|
@deletedInstance="onDeletedInstance"
|
||||||
|
></InstanceListExtended>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.series-details-table {
|
||||||
|
margin-top: var(--details-top-margin);
|
||||||
|
margin-left: 5%;
|
||||||
|
width: 95% !important;
|
||||||
|
background-color: var(--series-details-bg-color);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-details-table td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-details-table>:not(caption) >* >* {
|
||||||
|
background-color: var(--series-details-bg-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-details-table >* >* {
|
||||||
|
background-color: var(--series-details-bg-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
.series-button-group i {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
140
WebApplication/src/components/SeriesItem.vue
Normal file
140
WebApplication/src/components/SeriesItem.vue
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
<script>
|
||||||
|
import SeriesDetails from "./SeriesDetails.vue";
|
||||||
|
import bootstrap from "bootstrap/dist/js/bootstrap.bundle.min.js"
|
||||||
|
import { mapState, mapGetters } from "vuex"
|
||||||
|
import SourceType from '../helpers/source-type';
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["seriesId", "seriesInfo", 'studyMainDicomTags', 'patientMainDicomTags'],
|
||||||
|
emits: ['deletedSeries'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loaded: false,
|
||||||
|
expanded: false,
|
||||||
|
collapseElement: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
studiesSourceType: state => state.studies.sourceType,
|
||||||
|
}),
|
||||||
|
instancesCount() {
|
||||||
|
if (this.studiesSourceType == SourceType.LOCAL_ORTHANC) {
|
||||||
|
return this.seriesInfo.Instances.length;
|
||||||
|
} else if (this.studiesSourceType == SourceType.REMOTE_DICOM || this.studiesSourceType == SourceType.REMOTE_DICOM_WEB) {
|
||||||
|
return this.seriesInfo.MainDicomTags.NumberOfSeriesRelatedInstances;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$refs['series-collapsible-details'].addEventListener('show.bs.collapse', (e) => {
|
||||||
|
if (e.target == e.currentTarget) {
|
||||||
|
this.expanded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.$refs['series-collapsible-details'].addEventListener('hide.bs.collapse', (e) => {
|
||||||
|
if (e.target == e.currentTarget) {
|
||||||
|
this.expanded = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var el = this.$refs['series-collapsible-details'];
|
||||||
|
this.collapseElement = new bootstrap.Collapse(el, {toggle: false});
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(this.$route.query)) {
|
||||||
|
if (k === 'expand') {
|
||||||
|
if (v === 'series' || v === 'instance') {
|
||||||
|
this.collapseElement.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDeletedSeries() {
|
||||||
|
this.$emit("deletedSeries", this.seriesId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { SeriesDetails }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tbody>
|
||||||
|
<tr :class="{ 'series-row-collapsed': !expanded, 'series-row-expanded': expanded }">
|
||||||
|
<td></td>
|
||||||
|
<td
|
||||||
|
class="cut-text text-center"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
v-bind:data-bs-target="'#series-details-' + this.seriesId"
|
||||||
|
>{{ seriesInfo.MainDicomTags.SeriesNumber }}</td>
|
||||||
|
<td
|
||||||
|
class="cut-text"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
v-bind:data-bs-target="'#series-details-' + this.seriesId"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="seriesInfo.MainDicomTags.SeriesDescription"
|
||||||
|
>{{ seriesInfo.MainDicomTags.SeriesDescription }}</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="cut-text text-center"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
v-bind:data-bs-target="'#series-details-' + this.seriesId"
|
||||||
|
>{{ seriesInfo.MainDicomTags.Modality }}</td>
|
||||||
|
<td
|
||||||
|
class="cut-text text-center"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
v-bind:data-bs-target="'#series-details-' + this.seriesId"
|
||||||
|
>{{ instancesCount }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
class="collapse"
|
||||||
|
:class="{ 'series-details-collapsed': !expanded, 'series-details-expanded': expanded }"
|
||||||
|
v-bind:id="'series-details-' + this.seriesId"
|
||||||
|
ref="series-collapsible-details"
|
||||||
|
>
|
||||||
|
<td colspan="100">
|
||||||
|
<SeriesDetails
|
||||||
|
v-if="this.expanded"
|
||||||
|
:seriesId="this.seriesId"
|
||||||
|
:instancesIds="this.seriesInfo.Instances"
|
||||||
|
:seriesMainDicomTags="this.seriesInfo.MainDicomTags"
|
||||||
|
:studyMainDicomTags="this.studyMainDicomTags"
|
||||||
|
:patientMainDicomTags="this.patientMainDicomTags"
|
||||||
|
@deletedSeries="onDeletedSeries"
|
||||||
|
></SeriesDetails>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.series-row-collapsed {
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-row-expanded {
|
||||||
|
background-color: var(--series-selected-color);
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
border-top: 3px !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
border-color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-row-expanded>:first-child {
|
||||||
|
border-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-details-expanded {
|
||||||
|
background-color: var(--series-details-bg-color);
|
||||||
|
|
||||||
|
border-top: 0px !important;
|
||||||
|
border-bottom: 3px !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
border-color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
149
WebApplication/src/components/SeriesList.vue
Normal file
149
WebApplication/src/components/SeriesList.vue
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<script>
|
||||||
|
import axios from "axios"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
import SeriesItem from "./SeriesItem.vue"
|
||||||
|
import { translateDicomTag } from "../locales/i18n"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['studyId', 'patientMainDicomTags', 'studyMainDicomTags', 'studySeries'],
|
||||||
|
emits: ['deletedStudy'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
seriesInfo: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sortedSeriesIds() {
|
||||||
|
let keys = Object.keys(this.seriesInfo);
|
||||||
|
keys.sort((a, b) => (parseInt(this.seriesInfo[a].MainDicomTags.SeriesNumber) > parseInt(this.seriesInfo[b].MainDicomTags.SeriesNumber) ? 1 : -1))
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
studySeries(newValue, oldValue) {
|
||||||
|
for (const series of this.studySeries) {
|
||||||
|
this.seriesInfo[series["ID"]] = series;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
columnTitle(tagName) {
|
||||||
|
if (tagName == "instances_number") {
|
||||||
|
return "# " + this.$i18n.t('instances');
|
||||||
|
} else {
|
||||||
|
return translateDicomTag(this.$i18n.t, this.$i18n.te, tagName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
columnTooltip(tagName) {
|
||||||
|
if (tagName == "instances_number") {
|
||||||
|
return this.$i18n.t("instances_number");
|
||||||
|
} else {
|
||||||
|
return translateDicomTag(this.$i18n.t, this.$i18n.te, tagName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDeletedSeries(seriesId) {
|
||||||
|
delete this.seriesInfo[seriesId];
|
||||||
|
if (Object.keys(this.seriesInfo).length == 0) {
|
||||||
|
this.$emit("deletedStudy", this.studyId);
|
||||||
|
} else {
|
||||||
|
this.messageBus.emit('deleted-series-from-study-' + this.studyId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { SeriesItem }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table table-responsive table-sm series-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="2%" scope="col" class="series-table-header"></th>
|
||||||
|
<th
|
||||||
|
width="7%"
|
||||||
|
scope="col"
|
||||||
|
class="series-table-header cut-text"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
:title="columnTooltip('SeriesNumber')"
|
||||||
|
>{{columnTitle('SeriesNumber')}}</th>
|
||||||
|
<th
|
||||||
|
width="40%"
|
||||||
|
scope="col"
|
||||||
|
class="series-table-header cut-text"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
:title="columnTooltip('SeriesDescription')"
|
||||||
|
>{{columnTitle('SeriesDescription')}}</th>
|
||||||
|
<th
|
||||||
|
width="11%"
|
||||||
|
scope="col"
|
||||||
|
class="series-table-header cut-text text-center"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
:title="columnTooltip('Modality')"
|
||||||
|
>{{columnTitle('Modality')}}</th>
|
||||||
|
<th
|
||||||
|
width="5%"
|
||||||
|
scope="col"
|
||||||
|
class="series-table-header cut-text text-center"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
:title="columnTooltip('instances_number')"
|
||||||
|
>{{columnTitle('instances_number')}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<SeriesItem
|
||||||
|
v-for="seriesId in sortedSeriesIds"
|
||||||
|
:key="seriesId"
|
||||||
|
:seriesId="seriesId"
|
||||||
|
:seriesInfo="seriesInfo[seriesId]"
|
||||||
|
:studyMainDicomTags="this.studyMainDicomTags"
|
||||||
|
:patientMainDicomTags="this.patientMainDicomTags"
|
||||||
|
@deletedSeries="onDeletedSeries"
|
||||||
|
></SeriesItem>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
.series-table>:not(:first-child) {
|
||||||
|
border-top: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-table>:first-child {
|
||||||
|
border-bottom: 2px !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
border-color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-table {
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-table>:nth-child(odd) >* >* {
|
||||||
|
background-color: var(--series-odd-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-table>:nth-child(even) >* >* {
|
||||||
|
background-color: var(--series-even-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-table > tbody > tr:hover > *{
|
||||||
|
background-color: var(--series-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-table > tbody > tr.series-row-expanded:hover > * {
|
||||||
|
background-color: var(--series-details-bg-color);
|
||||||
|
}
|
||||||
|
.series-table > tbody > tr.series-details-expanded:hover > * {
|
||||||
|
background-color: var(--series-details-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.series-table-header {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.series-table td {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
221
WebApplication/src/components/Settings.vue
Normal file
221
WebApplication/src/components/Settings.vue
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [],
|
||||||
|
emits: [],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
verboseLevel: "default",
|
||||||
|
delayedDeletionPendingFilesCount: 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.verboseLevel = await api.getVerboseLevel();
|
||||||
|
if (this.hasDelayedDeletionPlugin) {
|
||||||
|
const status = await api.getDelayedDeletionStatus();
|
||||||
|
this.delayedDeletionPendingFilesCount = status["FilesPendingDeletion"];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async setVerboseLevel(level) {
|
||||||
|
this.verboseLevel = level;
|
||||||
|
await api.setVerboseLevel(level);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
tokens: state => state.configuration.tokens,
|
||||||
|
statistics: state => state.studies.statistics,
|
||||||
|
system: state => state.configuration.system,
|
||||||
|
installedPlugins: state => state.configuration.installedPlugins,
|
||||||
|
}),
|
||||||
|
totalDiskSize() {
|
||||||
|
if (this.statistics.TotalDiskSizeMB > 1024 * 1024) {
|
||||||
|
return (Math.round(this.statistics.TotalDiskSizeMB / (1024 * 1024) * 100) / 100) + " TB";
|
||||||
|
}
|
||||||
|
else if (this.statistics.TotalDiskSizeMB > 1024) {
|
||||||
|
return (Math.round(this.statistics.TotalDiskSizeMB / 1024 * 100) / 100) + " GB";
|
||||||
|
}
|
||||||
|
return this.statistics.TotalDiskSizeMB + " MB";
|
||||||
|
},
|
||||||
|
hasMaxPatientCount() {
|
||||||
|
return this.system.MaximumPatientCount > 0;
|
||||||
|
},
|
||||||
|
hasMaxStorageSize() {
|
||||||
|
return this.system.MaximumStorageSize > 0;
|
||||||
|
},
|
||||||
|
maxStorageSize() {
|
||||||
|
if (this.system.MaximumStorageSize > 1024 * 1024) {
|
||||||
|
return (Math.round(this.system.MaximumStorageSize / (1024 * 1024) * 100) / 100) + " TB";
|
||||||
|
}
|
||||||
|
else if (this.statistics.TotalDiskSizeMB > 1024) {
|
||||||
|
return (Math.round(this.system.MaximumStorageSize / 1024 * 100) / 100) + " GB";
|
||||||
|
}
|
||||||
|
return this.system.MaximumStorageSize + " MB";
|
||||||
|
},
|
||||||
|
hasDelayedDeletionPlugin() {
|
||||||
|
return 'delayed-deletion' in this.installedPlugins && this.installedPlugins['delayed-deletion'].Enabled;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="settings">
|
||||||
|
<div>
|
||||||
|
<h5>{{ $t('settings.statistics') }}</h5>
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="w-50 header"># {{ $t('patients') }}</th>
|
||||||
|
<td v-if="!hasMaxPatientCount" class="value">{{ statistics.CountPatients }}</td>
|
||||||
|
<td v-if="hasMaxPatientCount" class="value">{{ statistics.CountPatients }} / {{
|
||||||
|
system.MaximumPatientCount }} ({{ system.MaximumStorageMode }})</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="w-50 header"># {{ $t('studies') }}</th>
|
||||||
|
<td class="value">{{ statistics.CountStudies }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="header"># {{ $t('series_plural') }}</th>
|
||||||
|
<td class="value">{{ statistics.CountSeries }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="header"># {{ $t('instances') }}</th>
|
||||||
|
<td class="value">{{ statistics.CountInstances }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="header">{{ $t('settings.storage_size') }}</th>
|
||||||
|
<td v-if="!hasMaxStorageSize" class="value">{{ totalDiskSize }}</td>
|
||||||
|
<td v-if="hasMaxStorageSize" class="value">{{ totalDiskSize }} / {{ maxStorageSize }} ({{
|
||||||
|
system.MaximumStorageMode }})</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="hasDelayedDeletionPlugin">
|
||||||
|
<th scope="row" class="w-50 header"># {{ $t('plugins.delayed_deletion.pending_files_count') }}
|
||||||
|
</th>
|
||||||
|
<td class="value">{{ delayedDeletionPendingFilesCount }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5>{{ $t('settings.orthanc_system_info') }}</h5>
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="w-50 header">{{ $t('settings.orthanc_version') }}</th>
|
||||||
|
<td class="value">{{ system.Version }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="header">{{ $t('settings.dicom_AET') }}</th>
|
||||||
|
<td class="value">{{ system.DicomAet }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="header">{{ $t('settings.orthanc_name') }}</th>
|
||||||
|
<td class="value">{{ system.Name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="header">{{ $t('settings.dicom_port') }}</th>
|
||||||
|
<td class="value">{{ system.DicomPort }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="header">{{ $t('settings.ingest_transcoding') }}</th>
|
||||||
|
<td class="value">{{ system.IngestTranscoding }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="header">{{ $t('settings.overwrite_instances') }}</th>
|
||||||
|
<td class="value">{{ system.OverwriteInstances }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="header">{{ $t('settings.storage_compression') }}</th>
|
||||||
|
<td class="value">{{ system.StorageCompression }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="header">{{ $t('settings.read_only') }}</th>
|
||||||
|
<td class="value">{{ system.ReadOnly }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5>{{ $t('settings.verbosity_level') }}</h5>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn mx-2" @click="setVerboseLevel('default')"
|
||||||
|
:class="{ 'btn-primary': verboseLevel == 'default', 'btn-secondary': verboseLevel != 'default' }">{{
|
||||||
|
$t('default') }}</button>
|
||||||
|
<button type="button" class="btn mx-2" @click="setVerboseLevel('verbose')"
|
||||||
|
:class="{ 'btn-primary': verboseLevel == 'verbose', 'btn-secondary': verboseLevel != 'verbose' }">{{
|
||||||
|
$t('verbose') }}</button>
|
||||||
|
<button type="button" class="btn mx-2" @click="setVerboseLevel('trace')"
|
||||||
|
:class="{ 'btn-primary': verboseLevel == 'trace', 'btn-secondary': verboseLevel != 'trace' }">{{
|
||||||
|
$t('trace') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5>{{ $t('settings.installed_plugins') }}</h5>
|
||||||
|
<p class="m-2">{{ $t('settings.plugins_not_enabled') }} <span style="text-decoration: line-through;">{{
|
||||||
|
$t('settings.striked_through') }}</span></p>
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(configuration, plugin) in installedPlugins" :key="plugin"
|
||||||
|
:class="{ 'disabled-plugin': !configuration.Enabled }">
|
||||||
|
<th scope="row" class="w-25 header">{{ plugin }}</th>
|
||||||
|
<td class="w-50 value">{{ configuration.Description }}</td>
|
||||||
|
<td class="w-15 value">{{ configuration.Version }}</td>
|
||||||
|
<td class="w-15 value" v-if="!this.tokens.RequiredForLinks">
|
||||||
|
<!-- If tokens are required for links, no need to display links to plugin UI, they usually don't support links and won't work -->
|
||||||
|
<a v-if="configuration.RootUri && configuration.Enabled" type="button"
|
||||||
|
class="btn btn-primary" v-bind:href="configuration.RootUri">{{ $t('open') }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
h5 {
|
||||||
|
margin-top: 50px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-15 {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-plugin {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-text {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
222
WebApplication/src/components/SettingsLabels.vue
Normal file
222
WebApplication/src/components/SettingsLabels.vue
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
import LabelHelpers from "../helpers/label-helpers"
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [],
|
||||||
|
emits: [],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
availableLabels: [],
|
||||||
|
originalAvailableLabelsFromConfig: [],
|
||||||
|
allUsedLabels: [],
|
||||||
|
allOriginalLabels: [],
|
||||||
|
newLabel: "",
|
||||||
|
limitAvailableLabels: false,
|
||||||
|
originalLimitAvailableLabels: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
await this.reset();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async reset() {
|
||||||
|
let config = await api.getRolesConfig();
|
||||||
|
this.newLabel = "";
|
||||||
|
this.originalAvailableLabelsFromConfig = config["available-labels"];
|
||||||
|
this.allUsedLabels = await api.loadAllLabels();
|
||||||
|
|
||||||
|
if (this.originalAvailableLabelsFromConfig.length == 0) {
|
||||||
|
console.log("The available-labels list in the configuration is empty, getting the list of all current labels");
|
||||||
|
this.limitAvailableLabels = false;
|
||||||
|
this.allOriginalLabels = this.allUsedLabels;
|
||||||
|
this.availableLabels = [...this.allUsedLabels];
|
||||||
|
} else {
|
||||||
|
this.limitAvailableLabels = true;
|
||||||
|
this.availableLabels = [...this.originalAvailableLabelsFromConfig];
|
||||||
|
this.allOriginalLabels = [...this.originalAvailableLabelsFromConfig];
|
||||||
|
}
|
||||||
|
this.originalLimitAvailableLabels = this.limitAvailableLabels;
|
||||||
|
},
|
||||||
|
createLabel() {
|
||||||
|
if (this.availableLabels.indexOf(this.newLabel) == -1) { // ignore it if the label is already present in the list
|
||||||
|
this.availableLabels.push(this.newLabel);
|
||||||
|
this.availableLabels = this.availableLabels.sort();
|
||||||
|
}
|
||||||
|
this.newLabel = "";
|
||||||
|
},
|
||||||
|
deleteLabel(evetn, label) {
|
||||||
|
let index = this.availableLabels.indexOf(label);
|
||||||
|
if (index != -1) {
|
||||||
|
this.availableLabels.splice(index, 1);
|
||||||
|
}
|
||||||
|
// console.log(this.availableLabels);
|
||||||
|
// console.log(this.allOriginalLabels);
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
// reload the roles in case they have changed since we've loaded them and update the available-labels
|
||||||
|
let config = await api.getRolesConfig();
|
||||||
|
if (this.limitAvailableLabels) {
|
||||||
|
config["available-labels"] = this.availableLabels;
|
||||||
|
} else {
|
||||||
|
config["available-labels"] = [];
|
||||||
|
}
|
||||||
|
await api.setRolesConfig(config);
|
||||||
|
await this.$store.dispatch('configuration/load'); // to reload the available-labels
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
labelKeyPressed(event) {
|
||||||
|
// console.log(event);
|
||||||
|
let currentLabel = event.target.value;
|
||||||
|
this.newLabel = LabelHelpers.filterLabel(currentLabel);
|
||||||
|
// console.log(this.newLabel);
|
||||||
|
if (event.keyCode == 13) { // Enter = createLabel
|
||||||
|
this.createLabel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
canDeleteLabel(label) {
|
||||||
|
// we can delete a label only if it is currently not used
|
||||||
|
return this.allUsedLabels.indexOf(label) == -1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
}),
|
||||||
|
hasChanged() {
|
||||||
|
return (this.originalLimitAvailableLabels != this.limitAvailableLabels)
|
||||||
|
|| (this.availableLabels.length != this.allOriginalLabels.length)
|
||||||
|
|| !this.availableLabels.every((v, i) => v === this.allOriginalLabels[i]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="settings">
|
||||||
|
<div>
|
||||||
|
<h5>{{ $t('settings.available_labels_title') }}</h5>
|
||||||
|
<div class="w-100 p-2 mx-0">
|
||||||
|
<div class="form-check mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" value="" id="limit_labels"
|
||||||
|
v-model="limitAvailableLabels">
|
||||||
|
<label class="form-check-label" for="limit_labels">
|
||||||
|
{{ $t('settings.labels_limit_available_labels') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
<p class="instructions-text" v-html="$t('settings.available_labels_global_instructions_html')" :class="{'instructions-text-disabled': !limitAvailableLabels} "></p>
|
||||||
|
</div>
|
||||||
|
<div class="form-check" style="height: 3rem"><label for="new-label-input" class="form-label"><span
|
||||||
|
class="invalid-label-tips invalid-label-tips-hidden">{{ $t('labels.valid_alphabet_warning')
|
||||||
|
}}</span>
|
||||||
|
</label></div>
|
||||||
|
<div class="form-check d-flex">
|
||||||
|
<input id="new-label-input" type="text" class="form-control w-50" v-model="newLabel"
|
||||||
|
@keyup="labelKeyPressed($event)" :disabled="!limitAvailableLabels">
|
||||||
|
<button type="button" class="btn btn-sm btn-secondary m-1" @click="createLabel"
|
||||||
|
:disabled="!limitAvailableLabels">{{
|
||||||
|
$t('settings.create_new_label') }}</button>
|
||||||
|
</div>
|
||||||
|
<div class="labels-list mt-5">
|
||||||
|
<div v-for="label in availableLabels" :key="label"
|
||||||
|
class="labels-item border d-flex justify-content-between align-items-center">
|
||||||
|
<span class="list-group-item-with-button-content"
|
||||||
|
:class="{ 'disabled-label': !limitAvailableLabels }">{{ label }}</span>
|
||||||
|
<button type="button" class="btn labels-button" @click="deleteLabel($event, label)"
|
||||||
|
:disabled="!canDeleteLabel(label) || !limitAvailableLabels"><i
|
||||||
|
class="bi-trash"></i></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<button type="button" class="btn btn-primary mx-1" :disabled="!hasChanged" @click="save"> {{ $t('save')
|
||||||
|
}}</button>
|
||||||
|
<button type="button" class="btn btn-secondary mx-1" :disabled="!hasChanged" @click="cancel">{{
|
||||||
|
$t('cancel') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
h5 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
margin: 2px;
|
||||||
|
overflow: overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-item {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 15vw;
|
||||||
|
height: 3rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
padding-top: 0;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-label {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-15 {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-text {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-text-disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-with-button-content {
|
||||||
|
margin-top: var(--bs-list-group-item-padding-y);
|
||||||
|
margin-bottom: var(--bs-list-group-item-padding-y);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item-with-button {
|
||||||
|
padding-left: var(--bs-list-group-item-padding-x);
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-right: var(--bs-list-group-item-padding-x);
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-button {
|
||||||
|
/* background-color: #0002; */
|
||||||
|
border-width: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-button:disabled {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
427
WebApplication/src/components/SettingsPermissions.vue
Normal file
427
WebApplication/src/components/SettingsPermissions.vue
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
import bootstrap from "bootstrap/dist/js/bootstrap.bundle.min.js"
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [],
|
||||||
|
emits: [],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
allLabels: [],
|
||||||
|
selectedLabels: [],
|
||||||
|
allLabelPermissions: [],
|
||||||
|
allGlobalPermissions: ["admin-permissions", "edit-labels", "q-r-remote-modalities", "settings", "upload"],
|
||||||
|
selectedLabelPermissions: [],
|
||||||
|
selectedGlobalPermissions: [],
|
||||||
|
hasAllLabelsAllowed: false,
|
||||||
|
currentConfig: {},
|
||||||
|
originalConfig: {},
|
||||||
|
selectedRole: null,
|
||||||
|
hasChanged: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
var popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
|
||||||
|
var popoverList = popoverTriggerList.map(function (popoverTriggerEl) {
|
||||||
|
return new bootstrap.Popover(popoverTriggerEl)
|
||||||
|
});
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
hasAllLabelsAllowed(newValue, oldValue) {
|
||||||
|
if (newValue) {
|
||||||
|
this.selectedLabels = [];
|
||||||
|
}
|
||||||
|
this.updateHasChanged();
|
||||||
|
},
|
||||||
|
selectedRole(newValue, oldValue) {
|
||||||
|
this.updateHasChanged();
|
||||||
|
},
|
||||||
|
selectedLabels(newValue, oldValue) {
|
||||||
|
this.updateHasChanged();
|
||||||
|
},
|
||||||
|
selectedLabelPermissions(newValue, oldValue) {
|
||||||
|
this.updateHasChanged();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async reset() {
|
||||||
|
this.originalConfig = await api.getRolesConfig();
|
||||||
|
this.currentConfig = JSON.parse(JSON.stringify(this.originalConfig));
|
||||||
|
this.allLabelPermissions = this.filterLabelPermissions(await api.getPermissionsList(), false);
|
||||||
|
this.allGlobalPermissions = this.filterGlobalPermissions(await api.getPermissionsList(), false);
|
||||||
|
this.allLabels = this.currentConfig["available-labels"];
|
||||||
|
|
||||||
|
if (this.allLabels.length == 0) {
|
||||||
|
console.log("The available-labels list is empty, getting the list of all current labels");
|
||||||
|
this.allLabels = await api.loadAllLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedRole == null) {
|
||||||
|
let firstRole = Object.keys(this.currentConfig["roles"])[0];
|
||||||
|
this.selectRole(firstRole);
|
||||||
|
} else {
|
||||||
|
this.selectRole(this.selectedRole);
|
||||||
|
}
|
||||||
|
this.updateHasChanged();
|
||||||
|
},
|
||||||
|
filterLabelPermissions(permissionsList, expandAll) {
|
||||||
|
if (expandAll && permissionsList.indexOf("all") != -1) {
|
||||||
|
// replace "all" by the full list of label permissions
|
||||||
|
return [...this.allLabelPermissions];
|
||||||
|
} else {
|
||||||
|
let filtered = permissionsList.filter((perm) => this.allGlobalPermissions.indexOf(perm) == -1 && perm != "all");
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
filterGlobalPermissions(permissionsList, expandAll) {
|
||||||
|
if (expandAll && permissionsList.indexOf("all") != -1) {
|
||||||
|
// replace "all" by the full list of global permissions
|
||||||
|
return [...this.allGlobalPermissions];
|
||||||
|
} else {
|
||||||
|
let filtered = permissionsList.filter((perm) => this.allGlobalPermissions.indexOf(perm) != -1);
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getCurrentUiRoles() {
|
||||||
|
// write the definition of the role we are leaving into the "json" structure
|
||||||
|
if (this.selectedRole != null) {
|
||||||
|
let config = JSON.parse(JSON.stringify(this.currentConfig)); // make a copy
|
||||||
|
let permissions = [...this.selectedLabelPermissions].concat(this.selectedGlobalPermissions);
|
||||||
|
config["roles"][this.selectedRole]["permissions"] = permissions;
|
||||||
|
|
||||||
|
if (this.hasAllLabelsAllowed) {
|
||||||
|
config["roles"][this.selectedRole]["authorized-labels"] = ["*"];
|
||||||
|
} else {
|
||||||
|
config["roles"][this.selectedRole]["authorized-labels"] = this.selectedLabels;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
} else {
|
||||||
|
return this.currentConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
selectRole(role) {
|
||||||
|
if (this.selectedRole != null) {
|
||||||
|
// write the definition of the role we are leaving into the "json" structure
|
||||||
|
this.currentConfig = this.getCurrentUiRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedRole = role;
|
||||||
|
this.selectedLabelPermissions = this.filterLabelPermissions(this.currentConfig["roles"][this.selectedRole]["permissions"], true);
|
||||||
|
this.selectedGlobalPermissions = this.filterGlobalPermissions(this.currentConfig["roles"][this.selectedRole]["permissions"], true);
|
||||||
|
this.selectedLabels = this.currentConfig["roles"][this.selectedRole]["authorized-labels"];
|
||||||
|
this.hasAllLabelsAllowed = this.currentConfig["roles"][this.selectedRole]["authorized-labels"].indexOf("*") != -1;
|
||||||
|
},
|
||||||
|
isRoleSelected(role) {
|
||||||
|
return this.selectedRole == role;
|
||||||
|
},
|
||||||
|
isLabelSelected(label) {
|
||||||
|
return this.selectedLabels.indexOf(label) != -1;
|
||||||
|
},
|
||||||
|
toggleSelectedLabel(event, label) {
|
||||||
|
event.preventDefault();
|
||||||
|
let index = this.selectedLabels.indexOf(label);
|
||||||
|
if (index != -1) {
|
||||||
|
this.selectedLabels.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.selectedLabels.push(label);
|
||||||
|
}
|
||||||
|
this.updateHasChanged();
|
||||||
|
},
|
||||||
|
selectAllLabels() {
|
||||||
|
this.selectedLabels = [...this.allLabels];
|
||||||
|
},
|
||||||
|
clearSelectedLabels() {
|
||||||
|
this.selectedLabels = [];
|
||||||
|
},
|
||||||
|
isPermissionSelected(perm, labelOrGlobal) {
|
||||||
|
if (labelOrGlobal == "label") {
|
||||||
|
return this.selectedLabelPermissions.indexOf(perm) != -1;
|
||||||
|
} else {
|
||||||
|
return this.selectedGlobalPermissions.indexOf(perm) != -1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleSelectedPermission(event, perm, labelOrGlobal) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (labelOrGlobal == "label") {
|
||||||
|
let index = this.selectedLabelPermissions.indexOf(perm);
|
||||||
|
if (index != -1) {
|
||||||
|
this.selectedLabelPermissions.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.selectedLabelPermissions.push(perm);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let index = this.selectedGlobalPermissions.indexOf(perm);
|
||||||
|
if (index != -1) {
|
||||||
|
this.selectedGlobalPermissions.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.selectedGlobalPermissions.push(perm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.updateHasChanged();
|
||||||
|
},
|
||||||
|
clearSelectedLabelPermissions() {
|
||||||
|
this.selectedLabelPermissions = [];
|
||||||
|
},
|
||||||
|
selectAllLabelPermissions() {
|
||||||
|
this.selectedLabelPermissions = [...this.allLabelPermissions];
|
||||||
|
},
|
||||||
|
clearSelectedGlobalPermissions() {
|
||||||
|
this.selectedGlobalPermissions = [];
|
||||||
|
},
|
||||||
|
selectAllGlobalPermissions() {
|
||||||
|
this.selectedGlobalPermissions = [...this.allGlobalPermissions];
|
||||||
|
},
|
||||||
|
updateHasChanged() {
|
||||||
|
let original = JSON.stringify(this.originalConfig);
|
||||||
|
let current = JSON.stringify(this.getCurrentUiRoles());
|
||||||
|
console.log("hasChanged", this.originalConfig, this.getCurrentUiRoles());
|
||||||
|
console.log("hasChanged original ", original);
|
||||||
|
console.log("hasChanged current ", current);
|
||||||
|
return this.hasChanged = (original != current);
|
||||||
|
},
|
||||||
|
async save() {
|
||||||
|
// reload the available-labels in case they have changed since we've loaded them and update the roles
|
||||||
|
let fromApi = await api.getRolesConfig();
|
||||||
|
let current = this.getCurrentUiRoles();
|
||||||
|
current["available-labels"] = fromApi["available-labels"];
|
||||||
|
await api.setRolesConfig(current);
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
}),
|
||||||
|
labels() {
|
||||||
|
return this.allLabels;
|
||||||
|
},
|
||||||
|
isLabelsListEmpty() {
|
||||||
|
return this.allLabels.length == 0;
|
||||||
|
},
|
||||||
|
labelPermissions() {
|
||||||
|
return this.allLabelPermissions;
|
||||||
|
},
|
||||||
|
globalPermissions() {
|
||||||
|
return this.allGlobalPermissions;
|
||||||
|
},
|
||||||
|
allRoles() {
|
||||||
|
if (this.currentConfig && "roles" in this.currentConfig) {
|
||||||
|
return Object.keys(this.currentConfig["roles"]).sort();
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="settings">
|
||||||
|
<div class="container">
|
||||||
|
<h5>{{ $t('settings.permissions') }}</h5>
|
||||||
|
<p class="instructions" v-html="$t('settings.permissions_global_instructions_html')"></p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3 border-end border-3">
|
||||||
|
<h6><i class="bi bi-people-fill"></i> {{ $t('settings.roles_title') }}
|
||||||
|
<button class="btn btn-sm bi bi-info-circle" data-bs-toggle="popover" data-bs-trigger="focus"
|
||||||
|
:data-bs-content="$t('settings.roles_description_html')"></button>
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<h6>{{ $t('settings.labels_based_permissions_title') }}</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3 border-end border-3">
|
||||||
|
</div>
|
||||||
|
<div class="col-3">
|
||||||
|
<h6><i class="bi bi-key-fill"></i>{{ $t('settings.permissions') }}
|
||||||
|
<button class="btn btn-sm bi bi-info-circle" data-bs-toggle="popover" data-bs-trigger="focus"
|
||||||
|
:data-bs-content="$t('settings.permissions_instructions')"></button>
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<h6><i class="bi bi-tags-fill"></i> {{ $t('settings.labels_title') }}
|
||||||
|
<button class="btn btn-sm bi bi-info-circle" data-bs-toggle="popover" data-bs-trigger="focus"
|
||||||
|
:data-bs-content="$t('settings.labels_description_html')"></button>
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3 border-end border-3">
|
||||||
|
</div>
|
||||||
|
<div class="col-3 ">
|
||||||
|
<p class="">
|
||||||
|
<button type="button" class="btn btn-select btn-secondary" @click="selectAllLabelPermissions()"
|
||||||
|
data-bs-toggle="tooltip" :title="$t('settings.select_all_label_permissions')"><i
|
||||||
|
class="bi bi-list-check"></i></button>
|
||||||
|
<button type="button" class="btn btn-select btn-secondary"
|
||||||
|
@click="clearSelectedLabelPermissions()" data-bs-toggle="tooltip"
|
||||||
|
:title="$t('settings.permissions_clear_selection')"><i class="bi bi-x-circle"></i></button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<p class="">
|
||||||
|
<button type="button" class="btn btn-select btn-secondary" @click="selectAllLabels()"
|
||||||
|
:disabled="hasAllLabelsAllowed" data-bs-toggle="tooltip"
|
||||||
|
:title="$t('settings.labels_select_all_currents')"><i class="bi bi-list-check"></i></button>
|
||||||
|
<button type="button" class="btn btn-select btn-secondary" @click="clearSelectedLabels()"
|
||||||
|
:disabled="hasAllLabelsAllowed" data-bs-toggle="tooltip"
|
||||||
|
:title="$t('settings.labels_clear_selection')"><i class="bi bi-x-circle"></i></button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3 border-end border-3 permissions-body" style="height:auto">
|
||||||
|
<div class="list-group">
|
||||||
|
<button v-for="role in this.allRoles" :key="role" type="button"
|
||||||
|
class="list-group-item list-group-item-action" :class="{ 'active': isRoleSelected(role) }"
|
||||||
|
@click="selectRole(role)">{{ role }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-3 permissions-body">
|
||||||
|
<div class="list-group">
|
||||||
|
<label v-for="perm in labelPermissions" :key="perm" class="list-group-item"
|
||||||
|
:class="{ 'active': isPermissionSelected(perm, 'label') }"
|
||||||
|
@click="toggleSelectedPermission($event, perm, 'label')">
|
||||||
|
{{ perm }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" value="" id="all_future_labels"
|
||||||
|
v-model="hasAllLabelsAllowed">
|
||||||
|
<label class="form-check-label" for="all_future_labels">
|
||||||
|
{{ $t('settings.allow_all_currents_and_futures_labels') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="labels-list" :class="{'d-none': isLabelsListEmpty}">
|
||||||
|
<button v-for="label in labels" :key="label" @click="toggleSelectedLabel($event, label)"
|
||||||
|
class="labels-item border d-flex justify-content-between align-items-center" :disabled="hasAllLabelsAllowed" :class="{'is-label-selected': isLabelSelected(label), 'is-label-not-selected': !isLabelSelected(label)}">
|
||||||
|
<span class="list-group-item-with-button-content">{{ label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="labels-list mt-4" :class="{'d-none': !isLabelsListEmpty}">
|
||||||
|
<p class="w-100 text-center">{{ $t('settings.labels_list_empty') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3 pt-3 border-end border-3">
|
||||||
|
</div>
|
||||||
|
<div class="col-3 pt-3 permission-body-global border-top border-3">
|
||||||
|
<h6>{{ $t('settings.global_permissions_title') }}</h6>
|
||||||
|
<p class="">
|
||||||
|
<button type="button" class="btn btn-select btn-secondary" @click="selectAllGlobalPermissions()"
|
||||||
|
data-bs-toggle="tooltip" :title="$t('settings.select_all_global_permissions')"><i
|
||||||
|
class="bi bi-list-check"></i></button>
|
||||||
|
<button type="button" class="btn btn-select btn-secondary"
|
||||||
|
@click="clearSelectedGlobalPermissions()" data-bs-toggle="tooltip"
|
||||||
|
:title="$t('settings.permissions_clear_selection')"><i class="bi bi-x-circle"></i></button>
|
||||||
|
</p>
|
||||||
|
<div class="list-group">
|
||||||
|
<label v-for="perm in globalPermissions" :key="perm" class="list-group-item"
|
||||||
|
:class="{ 'active': isPermissionSelected(perm, 'global') }"
|
||||||
|
@click="toggleSelectedPermission($event, perm, 'global')">
|
||||||
|
{{ perm }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 pt-3 permission-body-global border-top border-3">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end mt-2 mb-4">
|
||||||
|
<button type="button" class="btn btn-primary mx-1" :disabled="!hasChanged" @click="save"> {{ $t('save')
|
||||||
|
}}</button>
|
||||||
|
<button type="button" class="btn btn-secondary mx-1" :disabled="!hasChanged" @click="cancel">{{
|
||||||
|
$t('cancel') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
h5 {
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-15 {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-text {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
.permissions-body {
|
||||||
|
height: 30vh;
|
||||||
|
overflow: overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
max-height: 35vh;
|
||||||
|
margin: 2px;
|
||||||
|
overflow: overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-item {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 10vw;
|
||||||
|
height: 3rem;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
padding-top: 0;
|
||||||
|
margin: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-15 {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-select {
|
||||||
|
margin: 0.2rem;
|
||||||
|
padding-left: 0.40rem;
|
||||||
|
padding-right: 0.40rem;
|
||||||
|
padding-top: 0.15rem;
|
||||||
|
padding-bottom: 0.15rem;
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-label-selected {
|
||||||
|
/* background-color: var(--bs-list-group-active-bg); */
|
||||||
|
background-color: var(--label-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-label-not-selected {
|
||||||
|
background-color: var(--bs-body-bg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
200
WebApplication/src/components/ShareModal.vue
Normal file
200
WebApplication/src/components/ShareModal.vue
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<script>
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import CopyToClipboardButton from "./CopyToClipboardButton.vue";
|
||||||
|
import resourceHelpers from "../helpers/resource-helpers"
|
||||||
|
import clipboardHelpers from "../helpers/clipboard-helpers"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["id", "orthancId", "studyMainDicomTags", "patientMainDicomTags"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
expirationInDays: 0,
|
||||||
|
shareLink: ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.reset();
|
||||||
|
|
||||||
|
this.$refs['modal-main-div'].addEventListener('show.bs.modal', (e) => {
|
||||||
|
// move the modal to body to avoid z-index issues: https://weblog.west-wind.com/posts/2016/sep/14/bootstrap-modal-dialog-showing-under-modal-background
|
||||||
|
document.querySelector('body').appendChild(e.target);
|
||||||
|
this.reset();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
expirationInDays(newValue, oldValue) {
|
||||||
|
this.shareLink = ""
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async reset() {
|
||||||
|
this.expirationInDays = this.uiOptions.DefaultShareDuration;
|
||||||
|
this.shareLink = "";
|
||||||
|
},
|
||||||
|
getDurationText(duration) {
|
||||||
|
if (duration == 0) {
|
||||||
|
return this.$i18n.t('share.never');
|
||||||
|
}
|
||||||
|
return "" + duration + " " + this.$i18n.t('share.days');
|
||||||
|
},
|
||||||
|
async share() {
|
||||||
|
let resourcesIds = [this.orthancId];
|
||||||
|
if (this.isBulkSelection) {
|
||||||
|
resourcesIds = this.selectedStudiesIds;
|
||||||
|
}
|
||||||
|
let token = await api.createToken({
|
||||||
|
tokenType: this.tokens.ShareType, // defined in configuration file
|
||||||
|
resourcesIds: resourcesIds,
|
||||||
|
level: 'study',
|
||||||
|
validityDuration: this.expirationInDays * 24 * 3600
|
||||||
|
})
|
||||||
|
this.shareLink = token["Url"];
|
||||||
|
},
|
||||||
|
copyAndClose() {
|
||||||
|
clipboardHelpers.copyToClipboard(this.shareLink);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
tokens: state => state.configuration.tokens,
|
||||||
|
selectedStudiesIds: state => state.studies.selectedStudiesIds,
|
||||||
|
}),
|
||||||
|
isBulkSelection() {
|
||||||
|
return !(this.patientMainDicomTags && this.studyMainDicomTags && Object.keys(this.patientMainDicomTags).length > 0 && Object.keys(this.studyMainDicomTags).length > 0);
|
||||||
|
},
|
||||||
|
resourceTitle() {
|
||||||
|
if (!this.isBulkSelection) {
|
||||||
|
return resourceHelpers.getResourceTitle("study", this.patientMainDicomTags, this.studyMainDicomTags, null, null);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
studiesCount() {
|
||||||
|
return this.selectedStudiesIds.length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { CopyToClipboardButton }
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal fade" :id="this.id" tabindex="-1" aria-labelledby="modalLabel" ref="modal-main-div">
|
||||||
|
<!-- aria-hidden="true" -->
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="modalLabel" v-if="!isBulkSelection">{{ $t("share.modal_title") + " " + resourceTitle }} </h5>
|
||||||
|
<h5 class="modal-title" id="modalLabel" v-if="isBulkSelection">{{ $t("share.modal_title_multiple_studies", { count: studiesCount}) }} </h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row py-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
{{ $t("share.expires_in") }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<select class="form-select" v-model="expirationInDays">
|
||||||
|
<option v-for="duration in uiOptions.ShareDurations" :key="duration" :value="duration">
|
||||||
|
{{ getDurationText(duration) }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row py-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
{{ $t("share.link") }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div id="parent" style="width:100%">
|
||||||
|
<input type="text" id="txt_input" v-model="shareLink" style="min-width: 100%!important;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<div class="d-flex flex-row-reverse">
|
||||||
|
<CopyToClipboardButton :valueToCopy="this.shareLink" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary"
|
||||||
|
data-bs-dismiss="modal">{{ $t("cancel") }}</button>
|
||||||
|
<button v-if="shareLink == ''" type="button" class="btn btn-primary"
|
||||||
|
@click="share()">{{ $t("share.button_title") }}</button>
|
||||||
|
<button v-if="shareLink != ''" type="button" class="btn btn-primary" data-bs-dismiss="modal"
|
||||||
|
@click="copyAndClose()">{{ $t("share.copy_and_close") }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* The switch - the box around the slider */
|
||||||
|
.switch {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 60px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide default HTML checkbox */
|
||||||
|
.switch input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The slider */
|
||||||
|
.slider {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: #ccc;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider:before {
|
||||||
|
position: absolute;
|
||||||
|
content: "";
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
left: 4px;
|
||||||
|
bottom: 4px;
|
||||||
|
background-color: white;
|
||||||
|
-webkit-transition: .4s;
|
||||||
|
transition: .4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider {
|
||||||
|
background-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus + .slider {
|
||||||
|
box-shadow: 0 0 1px #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + .slider:before {
|
||||||
|
-webkit-transform: translateX(26px);
|
||||||
|
-ms-transform: translateX(26px);
|
||||||
|
transform: translateX(26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rounded sliders */
|
||||||
|
.slider.round {
|
||||||
|
border-radius: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider.round:before {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
488
WebApplication/src/components/SideBar.vue
Normal file
488
WebApplication/src/components/SideBar.vue
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
<script>
|
||||||
|
|
||||||
|
import UploadHandler from "./UploadHandler.vue"
|
||||||
|
import JobsList from "./JobsList.vue";
|
||||||
|
import LanguagePicker from "./LanguagePicker.vue";
|
||||||
|
import { mapState, mapGetters } from "vuex"
|
||||||
|
import { orthancApiUrl, oe2ApiUrl } from "../globalConfigurations";
|
||||||
|
import api from "../orthancApi"
|
||||||
|
import SourceType from "../helpers/source-type";
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [],
|
||||||
|
emits: [],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// selectedModality: null,
|
||||||
|
selectedLabel: null,
|
||||||
|
modalitiesEchoStatus: {},
|
||||||
|
labelsStudyCount: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
userProfile: state => state.configuration.userProfile,
|
||||||
|
system: state => state.configuration.system,
|
||||||
|
queryableDicomModalities: state => state.configuration.queryableDicomModalities,
|
||||||
|
queryableDicomWebServers: state => state.configuration.queryableDicomWebServers,
|
||||||
|
studiesIds: state => state.studies.studiesIds,
|
||||||
|
statistics: state => state.studies.statistics,
|
||||||
|
labelFilters: state => state.studies.labelFilters,
|
||||||
|
jobs: state => state.jobs.jobsIds,
|
||||||
|
allLabels: state => state.labels.allLabels,
|
||||||
|
hasCustomLogo: state => state.configuration.hasCustomLogo,
|
||||||
|
configuration: state => state.configuration,
|
||||||
|
studiesSourceType: state => state.studies.sourceType,
|
||||||
|
studiesRemoteSource: state => state.studies.remoteSource,
|
||||||
|
hasExtendedFind: state => state.configuration.hasExtendedFind
|
||||||
|
}),
|
||||||
|
customLogoUrl() {
|
||||||
|
if (this.hasCustomLogo && this.configuration.customLogoUrl) {
|
||||||
|
return this.customLogoUrl;
|
||||||
|
} else {
|
||||||
|
return "./customizable/custom-logo";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasQueryableDicomWebServers() {
|
||||||
|
return this.queryableDicomWebServers.length > 0;
|
||||||
|
},
|
||||||
|
hasQueryableDicomModalities() {
|
||||||
|
return this.uiOptions.EnableDicomModalities && Object.keys(this.queryableDicomModalities).length > 0;
|
||||||
|
},
|
||||||
|
hasAccessToSettings() {
|
||||||
|
return this.uiOptions.EnableSettings;
|
||||||
|
},
|
||||||
|
hasAccessToSettingsLabelsAndPermissions() {
|
||||||
|
return this.hasAccessToSettings && this.uiOptions.EnablePermissionsEdition;
|
||||||
|
},
|
||||||
|
hasJobs() {
|
||||||
|
return this.jobs.length > 0;
|
||||||
|
},
|
||||||
|
hasLogout() {
|
||||||
|
return window.keycloak !== undefined;
|
||||||
|
},
|
||||||
|
hasUserProfile() {
|
||||||
|
return this.userProfile != null && this.userProfile.name;
|
||||||
|
},
|
||||||
|
displayedStudyCount() {
|
||||||
|
if (this.studiesSourceType == SourceType.LOCAL_ORTHANC) {
|
||||||
|
return this.studiesIds.length;
|
||||||
|
} else {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
orthancApiUrl() {
|
||||||
|
return orthancApiUrl;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isSelectedModality(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;
|
||||||
|
},
|
||||||
|
isEchoSuccess(modality) {
|
||||||
|
return this.modalitiesEchoStatus[modality] == true;
|
||||||
|
},
|
||||||
|
async selectLabel(label) {
|
||||||
|
this.selectedLabel = label;
|
||||||
|
await this.$store.dispatch('studies/updateSource', { 'source-type': SourceType.LOCAL_ORTHANC, 'remote-source': null });
|
||||||
|
this.messageBus.emit('filter-label-changed', label);
|
||||||
|
},
|
||||||
|
isSelectedLabel(label) {
|
||||||
|
return this.labelFilters.includes(label);
|
||||||
|
},
|
||||||
|
logout(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
let logoutOptions = {
|
||||||
|
"redirectUri": window.location.href
|
||||||
|
}
|
||||||
|
window.keycloak.logout(logoutOptions).then((success) => {
|
||||||
|
console.log("logout success", success);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("logout failed", error);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
changePassword(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.keycloak.login({ action: "UPDATE_PASSWORD" }).then((success) => {
|
||||||
|
console.log("login for password change success", success);
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error("login for password change failed", error);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async loadLabelsCount() {
|
||||||
|
if (Object.entries(this.labelsStudyCount).length == 0) {
|
||||||
|
for (const label of this.allLabels) {
|
||||||
|
this.labelsStudyCount[label] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.hasExtendedFind) {
|
||||||
|
if (this.uiOptions.EnableLabelsCount) {
|
||||||
|
for (const [k, v] of Object.entries(this.labelsStudyCount)) {
|
||||||
|
if (v == null) {
|
||||||
|
this.labelsStudyCount[k] = await api.getLabelStudyCount(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
allLabels(newValue, oldValue) {
|
||||||
|
this.loadLabelsCount();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadLabelsCount();
|
||||||
|
this.$refs['modalities-collapsible'].addEventListener('show.bs.collapse', (e) => {
|
||||||
|
for (const modality of Object.keys(this.queryableDicomModalities)) {
|
||||||
|
this.modalitiesEchoStatus[modality] = null;
|
||||||
|
}
|
||||||
|
for (const [modality, config] of Object.entries(this.queryableDicomModalities)) {
|
||||||
|
api.remoteModalityEcho(modality).then((response) => {
|
||||||
|
this.modalitiesEchoStatus[modality] = true;
|
||||||
|
}).catch(() => {
|
||||||
|
this.modalitiesEchoStatus[modality] = false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
components: { UploadHandler, JobsList, LanguagePicker },
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="nav-side-menu">
|
||||||
|
<div class="nav-side-content">
|
||||||
|
<div v-if="!hasCustomLogo">
|
||||||
|
<img class="orthanc-logo" src="../assets/images/orthanc.png"/>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasCustomLogo">
|
||||||
|
<img class="custom-logo" :src="customLogoUrl" />
|
||||||
|
</div>
|
||||||
|
<div v-if="hasCustomLogo">
|
||||||
|
<p class="powered-by-orthanc">
|
||||||
|
powered by
|
||||||
|
<img src="../assets/images/orthanc.png" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="uiOptions.ShowOrthancName" class="orthanc-name">
|
||||||
|
<p>{{ system.Name }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="menu-list">
|
||||||
|
<ul id="menu-content" class="menu-content collapse out">
|
||||||
|
<li class="d-flex align-items-center fix-router-link">
|
||||||
|
<router-link class="router-link" to="/">
|
||||||
|
<i class="fa fa-x-ray fa-lg menu-icon"></i>{{ $t('local_studies') }}
|
||||||
|
<span class="study-count ms-auto">{{ displayedStudyCount }} / {{ statistics.CountStudies
|
||||||
|
}}</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
<ul v-if="allLabels.length > 0" class="sub-menu" id="labels-list">
|
||||||
|
<li v-for="label in allLabels" :key="label"
|
||||||
|
v-bind:class="{ 'active': isSelectedLabel(label) }" @click="selectLabel(label)">
|
||||||
|
<i class="fa fa-tag label-icon"></i>
|
||||||
|
{{ label }}
|
||||||
|
<span class="study-count ms-auto">{{ labelsStudyCount[label] }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<li v-if="uiOptions.EnableUpload" class="d-flex align-items-center" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#upload-handler">
|
||||||
|
<i class="fa fa-file-upload fa-lg menu-icon"></i>{{ $t('upload') }}
|
||||||
|
<span class="ms-auto"></span>
|
||||||
|
</li>
|
||||||
|
<div v-if="uiOptions.EnableUpload" class="collapse" id="upload-handler">
|
||||||
|
<UploadHandler />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<li v-if="hasQueryableDicomModalities" class="d-flex align-items-center" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#modalities-list">
|
||||||
|
<i class="fa fa-radiation fa-lg menu-icon"></i>{{ $t('dicom_modalities') }}
|
||||||
|
<span class="arrow ms-auto"></span>
|
||||||
|
</li>
|
||||||
|
<ul class="sub-menu collapse" id="modalities-list" ref="modalities-collapsible">
|
||||||
|
<li v-for="modality of Object.keys(queryableDicomModalities)" :key="modality"
|
||||||
|
v-bind:class="{ 'active': this.isSelectedModality(modality) }">
|
||||||
|
<router-link class="router-link"
|
||||||
|
:to="{ path: '/filtered-studies', query: { 'source-type': 'dicom', 'remote-source': modality } }">
|
||||||
|
{{ modality }}
|
||||||
|
</router-link>
|
||||||
|
<span v-if="this.isEchoRunning(modality)" class="ms-auto spinner-border spinner-border-sm"
|
||||||
|
data-bs-toggle="tooltip" title="Checking connectivity"></span>
|
||||||
|
<span v-else-if="this.isEchoSuccess(modality)" class="ms-auto"><i
|
||||||
|
class="bi bi-check2 text-success echo-status" data-bs-toggle="tooltip"
|
||||||
|
title="C-Echo succeeded"></i></span>
|
||||||
|
<span v-else class="ms-auto"><i class="bi bi-x-lg text-danger echo-status" data-bs-toggle="tooltip"
|
||||||
|
title="C-Echo failed"></i></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<li v-if="hasQueryableDicomWebServers" class="d-flex align-items-center" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#dicomweb-servers-list">
|
||||||
|
<i class="fa fa-globe fa-lg menu-icon"></i>{{ $t('dicom_web_servers') }}
|
||||||
|
<span class="arrow ms-auto"></span>
|
||||||
|
</li>
|
||||||
|
<ul class="sub-menu collapse" id="dicomweb-servers-list">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<li v-if="hasAccessToSettings" class="d-flex align-items-center" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#settings-list">
|
||||||
|
<i class="fa fa-cogs fa-lg menu-icon"></i>{{ $t('settings.title') }}
|
||||||
|
<span class="arrow ms-auto"></span>
|
||||||
|
</li>
|
||||||
|
<ul class="sub-menu collapse" id="settings-list">
|
||||||
|
<li>
|
||||||
|
<router-link class="router-link" to="/settings">{{ $t('settings.system_info') }}</router-link>
|
||||||
|
</li>
|
||||||
|
<li v-if="hasAccessToSettingsLabelsAndPermissions">
|
||||||
|
<router-link class="router-link" to="/settings-labels">{{ $t('settings.available_labels_title') }}</router-link>
|
||||||
|
</li>
|
||||||
|
<li v-if="hasAccessToSettingsLabelsAndPermissions">
|
||||||
|
<router-link class="router-link" to="/settings-permissions">{{ $t('settings.permissions') }}</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<li v-if="uiOptions.EnableLinkToLegacyUi" class="d-flex align-items-center fix-router-link">
|
||||||
|
<a v-bind:href="this.orthancApiUrl + 'app/explorer.html'">
|
||||||
|
<i class="fa fa-solid fa-backward fa-lg menu-icon"></i>{{ $t('legacy_ui') }}
|
||||||
|
</a><span class="ms-auto"></span>
|
||||||
|
</li>
|
||||||
|
<li v-if="hasLogout" class="d-flex align-items-center" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#profile-list">
|
||||||
|
<i class="fa fa-user fa-lg menu-icon"></i><span v-if="hasUserProfile">{{ userProfile.name }}</span><span v-if="!hasUserProfile">{{ $t('profile') }}</span>
|
||||||
|
<span class="arrow ms-auto"></span>
|
||||||
|
</li>
|
||||||
|
<ul class="sub-menu collapse" id="profile-list" ref="profile-collapsible">
|
||||||
|
<li v-if="uiOptions.EnableChangePassword" class="d-flex align-items-center fix-router-link">
|
||||||
|
<a v-bind:href="'#'" @click="changePassword($event)">
|
||||||
|
<i class="fa fa-solid fa-key fa-lg menu-icon"></i>{{ $t('change_password') }}
|
||||||
|
</a><span class="ms-auto"></span>
|
||||||
|
</li>
|
||||||
|
<li v-if="hasLogout" class="d-flex align-items-center fix-router-link">
|
||||||
|
<a v-bind:href="'#'" @click="logout($event)">
|
||||||
|
<i class="fa fa-solid fa-arrow-right-from-bracket fa-lg menu-icon"></i>{{ $t('logout') }}
|
||||||
|
</a><span class="ms-auto"></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<li v-if="hasJobs" class="d-flex align-items-center">
|
||||||
|
<a href="#">
|
||||||
|
<i class="fa fa-solid fa-bars-progress fa-lg menu-icon"></i>{{ $t('my_jobs') }}
|
||||||
|
</a><span class="ms-auto"></span>
|
||||||
|
</li>
|
||||||
|
<div v-if="hasJobs" class="collapse show" id="jobs-list">
|
||||||
|
<JobsList />
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="bottom-side-bar">
|
||||||
|
<div class="bottom-side-bar-button">
|
||||||
|
<LanguagePicker/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.router-link {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fix-router-link {
|
||||||
|
margin-left: -20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.echo-status {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orthanc-name {
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orthanc-name p {
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orthanc-logo {
|
||||||
|
filter: brightness(50);
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.powered-by-orthanc > img {
|
||||||
|
filter: brightness(50);
|
||||||
|
max-width: 50%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-logo {
|
||||||
|
padding: 4px;
|
||||||
|
max-width: 90%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu {
|
||||||
|
font-family: verdana;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 200;
|
||||||
|
background-color: var(--nav-side-bg-color);
|
||||||
|
color: var(--nav-side-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-side-bar {
|
||||||
|
flex: 1;
|
||||||
|
align-self: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
min-height: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-side-bar-button { /* for the language picker */
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu ul,
|
||||||
|
.nav-side-menu li {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
line-height: 35px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu ul :not(collapsed) .arrow:before,
|
||||||
|
.nav-side-menu li :not(collapsed) .arrow:before {
|
||||||
|
font-family: "Font Awesome\ 5 Free";
|
||||||
|
font-weight: 900;
|
||||||
|
content: "\f0d7";
|
||||||
|
display: inline-block;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu li .study-count {
|
||||||
|
padding-right: 0px;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu ul .active,
|
||||||
|
.nav-side-menu li .active {
|
||||||
|
border-left: 3px solid var(--nav-side-active-border-color);
|
||||||
|
background-color: var(--nav-side-selected-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu ul .sub-menu li.active,
|
||||||
|
.nav-side-menu li .sub-menu li.active {
|
||||||
|
color: var(--nav-side-submenu-color);
|
||||||
|
background-color: var(--nav-side-selected-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu ul .sub-menu li.active a,
|
||||||
|
.nav-side-menu li .sub-menu li.active a {
|
||||||
|
color: var(--nav-side-submenu-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu ul .sub-menu li,
|
||||||
|
.nav-side-menu li .sub-menu li {
|
||||||
|
display: flex;
|
||||||
|
background-color: var(--nav-side-sub-bg-color);
|
||||||
|
border: none;
|
||||||
|
line-height: 28px;
|
||||||
|
border-bottom: 1px solid var(--nav-side-bg-color);
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu ul .sub-menu li:hover,
|
||||||
|
.nav-side-menu li .sub-menu li:hover {
|
||||||
|
border-left: 3px solid var(--nav-side-active-border-color);
|
||||||
|
background-color: var(--nav-side-selected-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu ul .sub-menu li:before,
|
||||||
|
.nav-side-menu li .sub-menu li:before {
|
||||||
|
font-family: "Font Awesome\ 5 Free";
|
||||||
|
font-weight: 900;
|
||||||
|
content: " ";
|
||||||
|
display: inline-block;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu li {
|
||||||
|
margin-left: -10px;
|
||||||
|
padding-left: 0px;
|
||||||
|
border-left: 3px solid var(--nav-side-bg-color);
|
||||||
|
border-bottom: 1px solid var(--nav-side-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu li a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--nav-side-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu li a i {
|
||||||
|
padding-left: 10px;
|
||||||
|
width: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu li:hover {
|
||||||
|
border-left: 3px solid var(--nav-side-active-border-color);
|
||||||
|
background-color: var(--nav-side-selected-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu .menu-list .menu-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-side-menu .menu-list .menu-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-list {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-icon {
|
||||||
|
width: 15px;
|
||||||
|
margin-right: 5px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
194
WebApplication/src/components/StudyDetails.vue
Normal file
194
WebApplication/src/components/StudyDetails.vue
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
<script>
|
||||||
|
import SeriesItem from "./SeriesItem.vue"
|
||||||
|
import SeriesList from "./SeriesList.vue";
|
||||||
|
import { mapState, mapGetters } from "vuex"
|
||||||
|
import ResourceButtonGroup from "./ResourceButtonGroup.vue";
|
||||||
|
import ResourceDetailText from "./ResourceDetailText.vue";
|
||||||
|
import api from "../orthancApi";
|
||||||
|
import LabelsEditor from "./LabelsEditor.vue";
|
||||||
|
import SourceType from '../helpers/source-type';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['studyId', 'studyMainDicomTags', 'patientMainDicomTags', 'labels'],
|
||||||
|
emits: ["deletedStudy", "studyLabelsUpdated"],
|
||||||
|
setup() {
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
samePatientStudiesCount: 0,
|
||||||
|
studySeries: [],
|
||||||
|
hasLoadedSamePatientsStudiesCount: false,
|
||||||
|
allLabelsLocalCopy: new Set(),
|
||||||
|
remoteStudyFoundLocally: false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.allLabelsLocalCopy = await api.loadAllLabels();
|
||||||
|
this.messageBus.on('added-series-to-study-' + this.studyId, this.reloadSeriesList);
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.samePatientStudiesCount = (await api.getSamePatientStudies(this.patientMainDicomTags, this.uiOptions.ShowSamePatientStudiesFilter)).length;
|
||||||
|
this.reloadSeriesList();
|
||||||
|
this.hasLoadedSamePatientsStudiesCount = true;
|
||||||
|
|
||||||
|
if (this.studiesSourceType == SourceType.REMOTE_DICOM || this.studiesSourceType == SourceType.REMOTE_DICOM_WEB) {
|
||||||
|
this.remoteStudyFoundLocally = (await api.studyExists(this.studyMainDicomTags.StudyInstanceUID));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
studiesSourceType: state => state.studies.sourceType,
|
||||||
|
studiesRemoteSource: state => state.studies.remoteSource,
|
||||||
|
}),
|
||||||
|
showLabels() {
|
||||||
|
if (this.studiesSourceType == SourceType.LOCAL_ORTHANC) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
samePatientStudiesLink() {
|
||||||
|
let filters = [];
|
||||||
|
for (let tag of this.uiOptions.ShowSamePatientStudiesFilter) {
|
||||||
|
if (tag in this.patientMainDicomTags) {
|
||||||
|
if (["PatientBirthDate"].indexOf(tag) >= 0) {
|
||||||
|
filters.push(tag + "=" + this.patientMainDicomTags[tag] + "");
|
||||||
|
} else {
|
||||||
|
filters.push(tag + "=\"" + this.patientMainDicomTags[tag] + "\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "/filtered-studies?" + filters.join('&');
|
||||||
|
},
|
||||||
|
isLocalOrthanc() {
|
||||||
|
return this.studiesSourceType == SourceType.LOCAL_ORTHANC;
|
||||||
|
},
|
||||||
|
isRemoteSource() {
|
||||||
|
return this.studiesSourceType == SourceType.REMOTE_DICOM || this.studiesSourceType == SourceType.REMOTE_DICOM_WEB;
|
||||||
|
},
|
||||||
|
sameLocalStudyLink() {
|
||||||
|
return "/filtered-studies?StudyInstanceUID=" + this.studyMainDicomTags.StudyInstanceUID;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { SeriesItem, SeriesList, ResourceButtonGroup, ResourceDetailText, LabelsEditor },
|
||||||
|
methods: {
|
||||||
|
onDeletedStudy() {
|
||||||
|
this.$emit("deletedStudy", this.studyId);
|
||||||
|
},
|
||||||
|
async reloadSeriesList() {
|
||||||
|
if (this.studiesSourceType == SourceType.LOCAL_ORTHANC) {
|
||||||
|
this.studySeries = (await api.getStudySeries(this.studyId));
|
||||||
|
} else if (this.studiesSourceType == SourceType.REMOTE_DICOM) {
|
||||||
|
let remoteSeries = (await api.remoteDicomFind("Series", this.studiesRemoteSource, {
|
||||||
|
"StudyInstanceUID": this.studyMainDicomTags.StudyInstanceUID,
|
||||||
|
"PatientID": this.patientMainDicomTags.PatientID,
|
||||||
|
"NumberOfSeriesRelatedInstances": "",
|
||||||
|
"Modality": "",
|
||||||
|
"SeriesDescription": "",
|
||||||
|
"SeriesNumber": ""
|
||||||
|
},
|
||||||
|
false /* isUnique */));
|
||||||
|
this.studySeries = remoteSeries.map(s => { return {
|
||||||
|
"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() {
|
||||||
|
this.$emit("studyLabelsUpdated", this.studyId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<table class="table table-responsive table-sm study-details-table">
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="showLabels && uiOptions.EnableEditLabels">
|
||||||
|
<td colspan="100%">
|
||||||
|
<LabelsEditor :labels="labels" :title="'labels.study_details_title'" :studyId="studyId" @labelsUpdated="labelsUpdated"></LabelsEditor>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="showLabels && !uiOptions.EnableEditLabels">
|
||||||
|
<td colspan="100%">
|
||||||
|
{{ $t('labels.study_details_title') }}
|
||||||
|
<span v-for="label in labelsModel" :key="label" class="label badge">{{ label }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="40%" class="cut-text">
|
||||||
|
<ul>
|
||||||
|
<ResourceDetailText v-for="tag in uiOptions.StudyMainTags" :key="tag" :tags="studyMainDicomTags"
|
||||||
|
:tag="tag" :showIfEmpty="true"></ResourceDetailText>
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td width="40%" class="cut-text">
|
||||||
|
<ul>
|
||||||
|
<ResourceDetailText v-for="tag in uiOptions.PatientMainTags" :key="tag" :tags="patientMainDicomTags"
|
||||||
|
:tag="tag" :showIfEmpty="true"></ResourceDetailText>
|
||||||
|
</ul>
|
||||||
|
<p v-if="isLocalOrthanc && hasLoadedSamePatientsStudiesCount && samePatientStudiesCount > 1" class="info-text">
|
||||||
|
{{ $t('this_patient_has_other_studies', { count: samePatientStudiesCount }) }}.
|
||||||
|
<router-link :to='samePatientStudiesLink' >
|
||||||
|
{{ $t('this_patient_has_other_studies_show') }}
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
<p v-if="isLocalOrthanc && hasLoadedSamePatientsStudiesCount && samePatientStudiesCount == 1" class="info-text">
|
||||||
|
{{ $t('this_patient_has_no_other_studies') }}
|
||||||
|
</p>
|
||||||
|
<p v-if="isRemoteSource && hasLoadedSamePatientsStudiesCount && samePatientStudiesCount > 1" class="info-text">
|
||||||
|
{{ $t('this_remote_patient_has_local_studies', { count: samePatientStudiesCount }) }}
|
||||||
|
<router-link :to='samePatientStudiesLink' >
|
||||||
|
{{ $t('this_patient_has_other_studies_show') }}
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
<p v-if="isRemoteSource && remoteStudyFoundLocally" class="info-text">
|
||||||
|
{{ $t('this_study_is_already_stored_locally') }}
|
||||||
|
<router-link :to='sameLocalStudyLink' >
|
||||||
|
{{ $t('this_study_is_already_stored_locally_show') }}
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="20%" class="study-button-group">
|
||||||
|
<ResourceButtonGroup :resourceOrthancId="this.studyId" :resourceLevel="'study'"
|
||||||
|
:patientMainDicomTags="this.patientMainDicomTags" :studyMainDicomTags="this.studyMainDicomTags"
|
||||||
|
:resourceDicomUid="this.studyMainDicomTags.StudyInstanceUID" :studySeries="this.studySeries" @deletedResource="onDeletedStudy">
|
||||||
|
</ResourceButtonGroup>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="100">
|
||||||
|
<SeriesList :studyId="this.studyId" :studyMainDicomTags="this.studyMainDicomTags"
|
||||||
|
:patientMainDicomTags="this.patientMainDicomTags" :studySeries="this.studySeries" @deletedStudy="onDeletedStudy"></SeriesList>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.study-button-group i {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
text-wrap: wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
328
WebApplication/src/components/StudyItem.vue
Normal file
328
WebApplication/src/components/StudyItem.vue
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
<script>
|
||||||
|
import SeriesList from "./SeriesList.vue"
|
||||||
|
import StudyDetails from "./StudyDetails.vue";
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import bootstrap from "bootstrap/dist/js/bootstrap.bundle.min.js"
|
||||||
|
import api from "../orthancApi";
|
||||||
|
import dateHelpers from "../helpers/date-helpers"
|
||||||
|
import SourceType from '../helpers/source-type';
|
||||||
|
import resourceHelpers from "../helpers/resource-helpers";
|
||||||
|
import TokenLinkButton from "./TokenLinkButton.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["studyId"],
|
||||||
|
emits: ["deletedStudy"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
study: {},
|
||||||
|
loaded: false,
|
||||||
|
expanded: false,
|
||||||
|
collapseElement: null,
|
||||||
|
selected: false,
|
||||||
|
pdfReports: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.messageBus.on('selected-all', this.onSelectedStudy);
|
||||||
|
this.messageBus.on('unselected-all', this.onUnselectedStudy);
|
||||||
|
this.messageBus.on('added-series-to-study-' + this.studyId, () => {this.onStudyUpdated(this.studyId)});
|
||||||
|
this.messageBus.on('deleted-series-from-study-' + this.studyId, () => {this.onStudyUpdated(this.studyId)});
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.study = this.studies.filter(s => s["ID"] == this.studyId)[0];
|
||||||
|
this.loaded = true;
|
||||||
|
this.seriesIds = this.study.Series;
|
||||||
|
this.selected = this.selectedStudiesIds.indexOf(this.studyId) != -1;
|
||||||
|
|
||||||
|
if (!this.$refs['study-collapsible-details']) {
|
||||||
|
console.log('no refs: ', studyResponse);
|
||||||
|
}
|
||||||
|
this.$refs['study-collapsible-details'].addEventListener('show.bs.collapse', (e) => {
|
||||||
|
if (e.target == e.currentTarget) {
|
||||||
|
this.expanded = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.$refs['study-collapsible-details'].addEventListener('hide.bs.collapse', (e) => {
|
||||||
|
if (e.target == e.currentTarget) {
|
||||||
|
this.expanded = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var el = this.$refs['study-collapsible-details'];
|
||||||
|
this.collapseElement = new bootstrap.Collapse(el, { toggle: false });
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(this.$route.query)) {
|
||||||
|
if (k === 'expand') {
|
||||||
|
if (v == null || v === 'study' || v === 'series' || v === 'instance') {
|
||||||
|
this.collapseElement.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasPdfReportIcon) {
|
||||||
|
let instances = await api.getStudyInstancesExpanded(this.study.ID, ["SOPClassUID", "SeriesDate", "SeriesDescription"]);
|
||||||
|
for (let instance of instances) {
|
||||||
|
if (instance.RequestedTags.SOPClassUID == "1.2.840.10008.5.1.4.1.1.104.1") {
|
||||||
|
let titles = [];
|
||||||
|
if (instance.RequestedTags.SeriesDate) {
|
||||||
|
titles.push(dateHelpers.formatDateForDisplay(instance.RequestedTags.SeriesDate, this.uiOptions.DateFormat));
|
||||||
|
}
|
||||||
|
if (instance.RequestedTags.SeriesDescription) {
|
||||||
|
titles.push(instance.RequestedTags.SeriesDescription);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pdfReports.push({
|
||||||
|
'url': api.getInstancePdfUrl(instance.ID),
|
||||||
|
'title': titles.join(' - '),
|
||||||
|
'id': instance.ID
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDeletedStudy(studyId) {
|
||||||
|
this.$emit("deletedStudy", this.studyId);
|
||||||
|
},
|
||||||
|
async onStudyUpdated(studyId) {
|
||||||
|
await this.$store.dispatch('studies/reloadStudy', {
|
||||||
|
'studyId': studyId,
|
||||||
|
'study': await api.getStudy(studyId)
|
||||||
|
})
|
||||||
|
this.study = this.studies.filter(s => s["ID"] == this.studyId)[0];
|
||||||
|
},
|
||||||
|
onSelectedStudy() {
|
||||||
|
this.selected = true;
|
||||||
|
},
|
||||||
|
onUnselectedStudy() {
|
||||||
|
this.selected = false;
|
||||||
|
},
|
||||||
|
async clickedSelect() {
|
||||||
|
// console.log(this.studyId, this.selected);
|
||||||
|
await this.$store.dispatch('studies/selectStudy', { studyId: this.studyId, isSelected: !this.selected }); // this.selected is the value before the click
|
||||||
|
this.selected = !this.selected;
|
||||||
|
// console.log(this.studyId, this.selected);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
studies: state => state.studies.studies,
|
||||||
|
studiesSourceType: state => state.studies.sourceType,
|
||||||
|
selectedStudiesIds: state => state.studies.selectedStudiesIds,
|
||||||
|
allLabels: state => state.labels.allLabels
|
||||||
|
}),
|
||||||
|
modalitiesInStudyForDisplay() {
|
||||||
|
if (this.study.RequestedTags.ModalitiesInStudy) {
|
||||||
|
return this.study.RequestedTags.ModalitiesInStudy.split('\\').join(',');
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showLabels() {
|
||||||
|
if (this.studiesSourceType == SourceType.LOCAL_ORTHANC) {
|
||||||
|
return !this.expanded && ((this.allLabels && this.allLabels.length > 0));
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasLabels() {
|
||||||
|
return this.study && this.study.Labels && this.study.Labels.length > 0;
|
||||||
|
},
|
||||||
|
formattedPatientName() {
|
||||||
|
return resourceHelpers.formatPatientName(this.study.PatientMainDicomTags.PatientName);
|
||||||
|
},
|
||||||
|
formattedPatientBirthDate() {
|
||||||
|
return dateHelpers.formatDateForDisplay(this.study.PatientMainDicomTags.PatientBirthDate, this.uiOptions.DateFormat);
|
||||||
|
},
|
||||||
|
formattedStudyDate() {
|
||||||
|
return dateHelpers.formatDateForDisplay(this.study.MainDicomTags.StudyDate, this.uiOptions.DateFormat);
|
||||||
|
},
|
||||||
|
seriesCount() {
|
||||||
|
if (this.study.sourceType == SourceType.REMOTE_DICOM || this.study.sourceType == SourceType.REMOTE_DICOM_WEB) {
|
||||||
|
return this.study.MainDicomTags.NumberOfStudyRelatedSeries;
|
||||||
|
} else if (this.study.Series) {
|
||||||
|
return this.study.Series.length;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
instancesCount() {
|
||||||
|
if (this.study.RequestedTags) {
|
||||||
|
return this.study.RequestedTags.NumberOfStudyRelatedInstances;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
seriesAndInstancesCount() {
|
||||||
|
const seriesCount = this.seriesCount;
|
||||||
|
const instancesCount = this.instancesCount;
|
||||||
|
if (instancesCount) {
|
||||||
|
return String(seriesCount) + "/" + String(instancesCount);
|
||||||
|
} else {
|
||||||
|
return seriesCount;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hasPdfReportIconPlaceholder() {
|
||||||
|
return this.studiesSourceType == SourceType.LOCAL_ORTHANC && this.uiOptions.EnableReportQuickButton && !this.hasPdfReportIcon;
|
||||||
|
},
|
||||||
|
hasPdfReportIcon() {
|
||||||
|
return this.study.RequestedTags.SOPClassesInStudy && this.study.RequestedTags.SOPClassesInStudy.indexOf("1.2.840.10008.5.1.4.1.1.104.1") != -1 && this.uiOptions.EnableReportQuickButton;
|
||||||
|
},
|
||||||
|
hasPrimaryViewerIconPlaceholder() {
|
||||||
|
return this.studiesSourceType == SourceType.LOCAL_ORTHANC && this.uiOptions.EnableViewerQuickButton && !this.hasPrimaryViewerIcon;
|
||||||
|
},
|
||||||
|
hasPrimaryViewerIcon() {
|
||||||
|
return this.studiesSourceType == SourceType.LOCAL_ORTHANC && this.primaryViewerUrl && this.uiOptions.EnableViewerQuickButton;
|
||||||
|
},
|
||||||
|
primaryViewerUrl() {
|
||||||
|
return resourceHelpers.getPrimaryViewerUrl("study", this.study.ID, this.study.MainDicomTags.StudyInstanceUID);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { SeriesList, StudyDetails, TokenLinkButton }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="loaded" :class="{ 'study-row-collapsed': !expanded, 'study-row-expanded': expanded, 'study-row-show-labels': showLabels }" @dblclick="doubleClicked">
|
||||||
|
<td>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" v-model="selected" @click="clickedSelect">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td v-if="hasPrimaryViewerIcon" class="td-viewer-icon">
|
||||||
|
<TokenLinkButton v-if="primaryViewerUrl"
|
||||||
|
level="study" :linkUrl="primaryViewerUrl"
|
||||||
|
:resourcesOrthancId="[study.ID]" linkType="icon"
|
||||||
|
iconClass="bi bi-eye-fill"
|
||||||
|
:tokenType="'viewer-instant-link'" :opensInNewTab="true">
|
||||||
|
</TokenLinkButton>
|
||||||
|
</td>
|
||||||
|
<td v-if="hasPrimaryViewerIconPlaceholder"></td>
|
||||||
|
<td v-if="hasPdfReportIcon" class="td-pdf-icon">
|
||||||
|
<TokenLinkButton v-for="pdfReport in pdfReports" :key="pdfReport.id"
|
||||||
|
level="study" :linkUrl="pdfReport.url"
|
||||||
|
:resourcesOrthancId="[study.ID]" linkType="icon"
|
||||||
|
iconClass="bi bi-file-earmark-text"
|
||||||
|
:tokenType="'download-instant-link'" :opensInNewTab="true"
|
||||||
|
:title="pdfReport.title">
|
||||||
|
</TokenLinkButton>
|
||||||
|
</td>
|
||||||
|
<td v-if="hasPdfReportIconPlaceholder"></td>
|
||||||
|
|
||||||
|
<td v-for="columnTag in uiOptions.StudyListColumns" :key="columnTag" class="cut-text"
|
||||||
|
:class="{ 'text-center': columnTag in ['modalities', 'seriesCount', 'instancesCount', 'seriesAndInstancesCount'] }" data-bs-toggle="collapse"
|
||||||
|
v-bind:data-bs-target="'#study-details-' + this.studyId">
|
||||||
|
<span v-if="columnTag == 'StudyDate'" data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="formattedStudyDate">{{ formattedStudyDate }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="columnTag == 'AccessionNumber'" data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="study.MainDicomTags.AccessionNumber">{{ study.MainDicomTags.AccessionNumber }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="columnTag == 'PatientID'" data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="study.PatientMainDicomTags.PatientID">{{ study.PatientMainDicomTags.PatientID }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="columnTag == 'PatientName'" data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="formattedPatientName">{{ formattedPatientName }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="columnTag == 'PatientBirthDate'" data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="formattedPatientBirthDate">{{ formattedPatientBirthDate }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="columnTag == 'StudyDescription'" data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="study.MainDicomTags.StudyDescription">{{ study.MainDicomTags.StudyDescription }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="columnTag == 'modalities'" data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="modalitiesInStudyForDisplay">{{
|
||||||
|
modalitiesInStudyForDisplay }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="columnTag == 'seriesCount'" data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="seriesCount">{{ seriesCount }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="columnTag == 'instancesCount'" data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="instancesCount">{{ instancesCount }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="columnTag == 'seriesAndInstancesCount'" data-bs-toggle="tooltip"
|
||||||
|
v-bind:title="seriesAndInstancesCount">{{ seriesAndInstancesCount }}
|
||||||
|
</span>
|
||||||
|
<span v-else>{{ study.MainDicomTags[columnTag] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-show="showLabels">
|
||||||
|
<td></td>
|
||||||
|
<td colspan="100%" class="label-row">
|
||||||
|
<span v-for="label in study.Labels" :key="label" class="label badge">{{ label }}</span>
|
||||||
|
<span v-if="!hasLabels"> </span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-show="loaded" class="collapse"
|
||||||
|
:class="{ 'study-details-collapsed': !expanded, 'study-details-expanded': expanded }"
|
||||||
|
v-bind:id="'study-details-' + this.studyId" ref="study-collapsible-details">
|
||||||
|
<td v-if="loaded && expanded" colspan="100">
|
||||||
|
<StudyDetails :studyId="this.studyId" :studyMainDicomTags="this.study.MainDicomTags"
|
||||||
|
:patientMainDicomTags="this.study.PatientMainDicomTags" :labels="this.study.Labels" @deletedStudy="onDeletedStudy" @studyLabelsUpdated="onStudyUpdated"></StudyDetails>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.study-row-collapsed {
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-row-expanded {
|
||||||
|
background-color: var(--study-details-bg-color);
|
||||||
|
font-weight: 700;
|
||||||
|
|
||||||
|
border-top: 3px !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
border-color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-row-expanded>:first-child {
|
||||||
|
border-bottom: 5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-row-show-labels {
|
||||||
|
border-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.study-table>tbody>tr.study-row-expanded:hover {
|
||||||
|
background-color: var(--study-details-bg-color);
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-table>tbody>tr.study-details-expanded:hover {
|
||||||
|
background-color: var(--study-details-bg-color);
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.study-details-expanded {
|
||||||
|
background-color: var(--study-details-bg-color);
|
||||||
|
|
||||||
|
border-top: 0px !important;
|
||||||
|
border-bottom: 3px !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
border-color: black !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-row {
|
||||||
|
border-top: 0px !important;
|
||||||
|
border-bottom: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.td-viewer-icon {
|
||||||
|
padding: 0; /* to maximize click space for the icon */
|
||||||
|
}
|
||||||
|
|
||||||
|
.td-pdf-icon {
|
||||||
|
padding: 0; /* to maximize click space for the icon */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1232
WebApplication/src/components/StudyList.vue
Normal file
1232
WebApplication/src/components/StudyList.vue
Normal file
File diff suppressed because it is too large
Load Diff
84
WebApplication/src/components/TagsTree.vue
Normal file
84
WebApplication/src/components/TagsTree.vue
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<script>
|
||||||
|
import CopyToClipboardButton from "./CopyToClipboardButton.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['tags'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
},
|
||||||
|
components : { CopyToClipboardButton }
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(item, key) in tags" :key="key">
|
||||||
|
<span v-if="item.Type=='Sequence'">
|
||||||
|
<span class="details-label">{{key}} - {{item.Name}}: </span>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(subItem, subIndex) in item.Value" :key="subItem">
|
||||||
|
Item {{subIndex + 1}}
|
||||||
|
<tags-tree
|
||||||
|
:tags="subItem">
|
||||||
|
</tags-tree>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
<span v-else class="d-flex w-100">
|
||||||
|
<span class="details-label">{{key}} - {{item.Name}}: </span>
|
||||||
|
<span v-if="item.Type=='String'" class="details">{{item.Value}}</span>
|
||||||
|
<CopyToClipboardButton :valueToCopy="item.Value" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.details-label {
|
||||||
|
font-weight: 700;
|
||||||
|
max-width: 30%;
|
||||||
|
/* overflow: hidden;
|
||||||
|
text-overflow: ellipsis; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
margin-left: auto !important;
|
||||||
|
font-weight: 500;
|
||||||
|
/* overflow: hidden;
|
||||||
|
text-overflow: ellipsis; */
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: #80808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
display: block;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: #80808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul:last-child {
|
||||||
|
border:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li:last-child {
|
||||||
|
border:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
57
WebApplication/src/components/TokenLanding.vue
Normal file
57
WebApplication/src/components/TokenLanding.vue
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<script>
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [],
|
||||||
|
async created() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const response = await api.parseToken('token', params.get('token'));
|
||||||
|
this.tokenChecked = true;
|
||||||
|
|
||||||
|
if ('ErrorCode' in response) {
|
||||||
|
this.errorCode = response['ErrorCode'];
|
||||||
|
} else if ('RedirectUrl' in response) {
|
||||||
|
window.location = response['RedirectUrl'];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tokenChecked: false,
|
||||||
|
errorCode: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
tokens: state => state.configuration.tokens,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
components: {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="d-flex flex-column min-vh-100 justify-content-center align-items-center h4 text-center">
|
||||||
|
<span v-if="!tokenChecked">
|
||||||
|
<p v-html="$t('token.token_being_checked_html')"></p>
|
||||||
|
<div class="spinner-border" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<p v-if="tokenChecked && errorCode == 'invalid'" v-html="$t('token.error_token_invalid_html')"></p>
|
||||||
|
<p v-if="tokenChecked && errorCode == 'expired'" v-html="$t('token.error_token_expired_html')"></p>
|
||||||
|
<p v-if="tokenChecked && errorCode == 'unknown'" v-html="$t('token.error_token_unknown_html')"></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style></style>
|
||||||
142
WebApplication/src/components/TokenLinkButton.vue
Normal file
142
WebApplication/src/components/TokenLinkButton.vue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<script>
|
||||||
|
import Modal from "./Modal.vue"
|
||||||
|
import ShareModal from "./ShareModal.vue"
|
||||||
|
import ModifyModal from "./ModifyModal.vue"
|
||||||
|
import $ from "jquery"
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["resourcesOrthancId", "linkUrl", "level", "tokenType", "validityDuration", "title", "iconClass", "opensInNewTab", "linkType", "disabled"],
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async clicked(event) {
|
||||||
|
if (!this.tokens.RequiredForLinks) {
|
||||||
|
return; // just execute the default click handler
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
let validityDuration = this.validityDuration;
|
||||||
|
if (validityDuration == null || validityDuration === undefined) {
|
||||||
|
validityDuration = this.tokens.InstantLinksValidity;
|
||||||
|
}
|
||||||
|
let level = this.level;
|
||||||
|
if (level == "bulk-study") {
|
||||||
|
level = "study";
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = await api.createToken({ tokenType: this.tokenType, resourcesIds: this.resourcesOrthancId, level: level, validityDuration: validityDuration });
|
||||||
|
let finalUrl = this.linkUrl;
|
||||||
|
|
||||||
|
// give priority to the urls coming from the token service
|
||||||
|
if (token["Url"] != null) {
|
||||||
|
finalUrl = token["Url"];
|
||||||
|
}
|
||||||
|
else if (this.linkUrl.indexOf('?') == -1) {
|
||||||
|
finalUrl = finalUrl + "?token=" + token["Token"];
|
||||||
|
} else {
|
||||||
|
finalUrl = finalUrl + "&token=" + token["Token"];
|
||||||
|
}
|
||||||
|
|
||||||
|
let link = document.querySelector("#hidden-link-for-tokens");
|
||||||
|
if (link == null) {
|
||||||
|
link = document.createElement('a');
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.setAttribute("id", "hidden-link-for-tokens");
|
||||||
|
link.setAttribute("type", "hidden");
|
||||||
|
}
|
||||||
|
link.target = this.target;
|
||||||
|
link.href = finalUrl;
|
||||||
|
link.click();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
tokens: state => state.configuration.tokens,
|
||||||
|
}),
|
||||||
|
target() {
|
||||||
|
if (this.opensInNewTab === undefined || this.opensInNewTab == false) {
|
||||||
|
return "_self";
|
||||||
|
} else {
|
||||||
|
return "_blank";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isButton() {
|
||||||
|
return this.linkType === undefined || this.linkType == "button";
|
||||||
|
},
|
||||||
|
isDropDownItem() {
|
||||||
|
return this.linkType == "dropdown-item";
|
||||||
|
},
|
||||||
|
isIcon() {
|
||||||
|
return this.linkType == "icon";
|
||||||
|
},
|
||||||
|
classes() {
|
||||||
|
if (this.disabled) {
|
||||||
|
return "btn-secondary disabled-link";
|
||||||
|
} else {
|
||||||
|
return "btn-secondary";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
divClasses() {
|
||||||
|
if (this.linkType == "icon") {
|
||||||
|
return "div-icon"
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="divClasses">
|
||||||
|
<a v-if="isButton" class="btn btn-sm m-1" type="button"
|
||||||
|
data-bs-toggle="tooltip" :title="title" @click="clicked" :target="target" :href="linkUrl" :class="classes">
|
||||||
|
<i :class="iconClass"></i>
|
||||||
|
</a>
|
||||||
|
<a v-if="isDropDownItem" class="dropdown-item" :target="target" @click="clicked"
|
||||||
|
:href="linkUrl">{{ title }}
|
||||||
|
</a>
|
||||||
|
<a v-if="isIcon" class="icon" :target="target" @click="clicked"
|
||||||
|
data-bs-toggle="tooltip" :title="title" :href="linkUrl">
|
||||||
|
<i :class="iconClass" style="padding-top: 4px;"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* this mimicks the disabled state of btn-secondary */
|
||||||
|
.disabled-link {
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: #6c757d;
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--bs-table-color);
|
||||||
|
width: 100%; /* increase the size of the clickable area */
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.div-icon {
|
||||||
|
width: 100%; /* increase the size of the clickable area */
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
216
WebApplication/src/components/UploadHandler.vue
Normal file
216
WebApplication/src/components/UploadHandler.vue
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
<script>
|
||||||
|
import {uppie} from "uppie"
|
||||||
|
import UploadReport from "./UploadReport.vue"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
|
||||||
|
// Drop handler function to get all files
|
||||||
|
async function getAllFileEntries(dataTransferItemList) {
|
||||||
|
let fileEntries = [];
|
||||||
|
// Use BFS to traverse entire directory/file structure
|
||||||
|
let queue = [];
|
||||||
|
// Unfortunately dataTransferItemList is not iterable i.e. no forEach
|
||||||
|
for (let i = 0; i < dataTransferItemList.length; i++) {
|
||||||
|
queue.push(dataTransferItemList[i].webkitGetAsEntry());
|
||||||
|
}
|
||||||
|
while (queue.length > 0) {
|
||||||
|
let entry = queue.shift();
|
||||||
|
if (entry.isFile) {
|
||||||
|
fileEntries.push(entry);
|
||||||
|
} else if (entry.isDirectory) {
|
||||||
|
let reader = entry.createReader();
|
||||||
|
queue.push(...await readAllDirectoryEntries(reader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileFromFileEntry(fileEntry) {
|
||||||
|
try {
|
||||||
|
return await new Promise((resolve, reject) => fileEntry.file(resolve, reject));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('getFileFromFileEntry', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAllFiles(dataTransferItemList) {
|
||||||
|
let fileEntries = await getAllFileEntries(dataTransferItemList);
|
||||||
|
|
||||||
|
let files = [];
|
||||||
|
for (let fileEntry of fileEntries) {
|
||||||
|
files.push(await getFileFromFileEntry(fileEntry));
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all the entries (files or sub-directories) in a directory by calling readEntries until it returns empty array
|
||||||
|
async function readAllDirectoryEntries(directoryReader) {
|
||||||
|
let entries = [];
|
||||||
|
let readEntries = await readEntriesPromise(directoryReader);
|
||||||
|
while (readEntries.length > 0) {
|
||||||
|
entries.push(...readEntries);
|
||||||
|
readEntries = await readEntriesPromise(directoryReader);
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap readEntries in a promise to make working with readEntries easier
|
||||||
|
async function readEntriesPromise(directoryReader) {
|
||||||
|
try {
|
||||||
|
return await new Promise((resolve, reject) => {
|
||||||
|
directoryReader.readEntries(resolve, reject);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('readEntriesPromise', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFileAsync(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = () => {
|
||||||
|
resolve(reader.result);
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = reject;
|
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
uploadCounter: 0,
|
||||||
|
lastUploadReports: {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
uppie(document.querySelector("#filesUpload"), this.uppieUploadHandler);
|
||||||
|
uppie(document.querySelector("#foldersUpload"), this.uppieUploadHandler);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDrop(ev) {
|
||||||
|
// console.log("on drop", ev);
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
getAllFiles(ev.dataTransfer.items).then((files) => {
|
||||||
|
this.uploadFiles(files);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
},
|
||||||
|
onDeletedUploadReport(uploadReportId) {
|
||||||
|
delete this.lastUploadReports[uploadReportId];
|
||||||
|
},
|
||||||
|
async uploadedFile(uploadId, uploadedFileResponse) {
|
||||||
|
let studyId = uploadedFileResponse["ParentStudy"];
|
||||||
|
if (!this.lastUploadReports[uploadId].uploadedStudiesIds.has(studyId)) {
|
||||||
|
this.lastUploadReports[uploadId].uploadedStudiesIds.add(studyId);
|
||||||
|
const studyResponse = await api.getStudy(studyId);
|
||||||
|
this.lastUploadReports[uploadId].uploadedStudies[studyId] = studyResponse;
|
||||||
|
|
||||||
|
this.$store.dispatch('studies/addStudy', { study: studyResponse, studyId: studyId, reloadStats: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async uploadFiles(files) {
|
||||||
|
let uploadId = this.uploadCounter++;
|
||||||
|
|
||||||
|
this.lastUploadReports[uploadId] = {
|
||||||
|
id: uploadId,
|
||||||
|
filesCount: files.length,
|
||||||
|
successFilesCount: 0,
|
||||||
|
failedFilesCount: 0,
|
||||||
|
skippedFilesCount: 0,
|
||||||
|
uploadedStudiesIds: new Set(),
|
||||||
|
uploadedStudies: {}, // studies as returned by tools/find
|
||||||
|
errorMessages: {}
|
||||||
|
};
|
||||||
|
for (let file of files) {
|
||||||
|
let filename = file.webkitRelativePath || file.name;
|
||||||
|
if (file.name == "DICOMDIR") {
|
||||||
|
console.log("upload: skipping DICOMDIR file");
|
||||||
|
this.lastUploadReports[uploadId].skippedFilesCount++;
|
||||||
|
this.lastUploadReports[uploadId].errorMessages[filename] = "skipped";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fileContent = await readFileAsync(file);
|
||||||
|
try {
|
||||||
|
const uploadResponse = await api.uploadFile(fileContent);
|
||||||
|
|
||||||
|
if (Array.isArray(uploadResponse)) { // we have uploaded a zip
|
||||||
|
|
||||||
|
if (uploadResponse.length > 0) {
|
||||||
|
this.lastUploadReports[uploadId].successFilesCount++;
|
||||||
|
for (let uploadFileResponse of uploadResponse) {
|
||||||
|
this.uploadedFile(uploadId, uploadFileResponse);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.lastUploadReports[uploadId].failedFilesCount++;
|
||||||
|
this.lastUploadReports[uploadId].errorMessages[filename] = "no valid DICOM files found in zip";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.lastUploadReports[uploadId].successFilesCount++;
|
||||||
|
this.uploadedFile(uploadId, uploadResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('uploadFiles', error);
|
||||||
|
let errorMessage = "error " + error.response.status;
|
||||||
|
if (error.response.status >= 400 && error.response.status < 500) {
|
||||||
|
errorMessage = error.response.data.Message;
|
||||||
|
}
|
||||||
|
this.lastUploadReports[uploadId].failedFilesCount++;
|
||||||
|
this.lastUploadReports[uploadId].errorMessages[filename] = errorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async uppieUploadHandler(event, formData, files) {
|
||||||
|
await this.uploadFiles(event.target.files);
|
||||||
|
|
||||||
|
// reset input for next upload
|
||||||
|
document.getElementById('filesUpload').value = null;
|
||||||
|
document.getElementById('foldersUpload').value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { UploadReport }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="upload-handler-drop-zone" @drop="this.onDrop" @dragover="this.onDragOver">
|
||||||
|
<div class="mb-3">{{ $t('drop_files') }}</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="btn btn-primary btn-file">
|
||||||
|
{{ $t('select_folder') }} <input type="file" style="display: none;" id="foldersUpload" required
|
||||||
|
multiple directory webkitdirectory allowdirs>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="btn btn-primary btn-file">
|
||||||
|
{{ $t('select_files') }} <input type="file" style="display: none;" id="filesUpload" required multiple>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-report-list">
|
||||||
|
<UploadReport v-for="(upload, key) in lastUploadReports" :report="upload" :key="key"
|
||||||
|
@deletedUploadReport="onDeletedUploadReport"></UploadReport>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
.upload-report-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-handler-drop-zone {
|
||||||
|
border-color: white;
|
||||||
|
border-style: dashed;
|
||||||
|
border-width: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
109
WebApplication/src/components/UploadReport.vue
Normal file
109
WebApplication/src/components/UploadReport.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<script>
|
||||||
|
import Modal from "./Modal.vue"
|
||||||
|
import { mapState } from "vuex"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["report"],
|
||||||
|
emits: ["deletedUploadReport"],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
uiOptions: state => state.configuration.uiOptions,
|
||||||
|
}),
|
||||||
|
pctSuccess() {
|
||||||
|
return 100.0 * this.report.successFilesCount / this.report.filesCount;
|
||||||
|
},
|
||||||
|
pctSkipped() {
|
||||||
|
return 100.0 * this.report.skippedFilesCount / this.report.filesCount;
|
||||||
|
},
|
||||||
|
pctFailed() {
|
||||||
|
return 100.0 * this.report.failedFilesCount / this.report.filesCount;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close(reportId) {
|
||||||
|
this.$emit("deletedUploadReport", reportId);
|
||||||
|
},
|
||||||
|
getStudyLine(studyId, studyMainDicomTags, patientMainDicomTags) {
|
||||||
|
// format the line to display for each study
|
||||||
|
let infos = [];
|
||||||
|
|
||||||
|
for (let tag of this.uiOptions.UploadReportTags) {
|
||||||
|
if (tag in studyMainDicomTags && studyMainDicomTags[tag] && studyMainDicomTags[tag].length > 0) {
|
||||||
|
infos.push(studyMainDicomTags[tag]);
|
||||||
|
} else if (tag in patientMainDicomTags && patientMainDicomTags[tag] && patientMainDicomTags[tag].length > 0) {
|
||||||
|
infos.push(patientMainDicomTags[tag]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (infos.length == 0) { // if nothing to display, display the study id
|
||||||
|
infos.push(studyId.slice(0, 20) + "...");
|
||||||
|
}
|
||||||
|
return infos.slice(0, this.uiOptions.UploadReportMaxTags).join(" - ");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { Modal }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card border-secondary job-card">
|
||||||
|
<div class="card-header jobs-header">
|
||||||
|
{{ $t('upload') }} {{ report.filesCount }} {{ $t('files') }}
|
||||||
|
<button type="button" class="btn-close job-card-close" aria-label="Close"
|
||||||
|
@click="close(report.id)"></button>
|
||||||
|
<div class="progress mt-1 mb-1" style="width:90%">
|
||||||
|
<div class="progress-bar bg-success" role="progressbar"
|
||||||
|
v-bind:style="'width: ' + this.pctSuccess + '%'"></div>
|
||||||
|
<div class="progress-bar bg-secondary" role="progressbar"
|
||||||
|
v-bind:style="'width: ' + this.pctSkipped + '%'"></div>
|
||||||
|
<div class="progress-bar bg-danger" role="progressbar" v-bind:style="'width: ' + this.pctFailed + '%'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="Object.keys(report.errorMessages).length > 0">
|
||||||
|
<button class="btn btn-sm btn-secondary m-1" type="button" data-bs-toggle="modal"
|
||||||
|
v-bind:data-bs-target="'#upload-errors-modal-' + this.report.id">{{ $t('show_errors') }}</button>
|
||||||
|
<Modal :id="'upload-errors-modal-' + this.report.id" :headerText="'Upload error report'"
|
||||||
|
:okText="'Close'" @ok="close($event)">
|
||||||
|
<template #modalBody>
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="50%">{{ $t('file') }}</th>
|
||||||
|
<th width="50%">{{ $t('error') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(error, file) in report.errorMessages" :key="error">
|
||||||
|
<td>{{ file }}</td>
|
||||||
|
<td class="text-right">{{ error }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body text-secondary jobs-body">
|
||||||
|
<p class="card-text">
|
||||||
|
<span class="upload-details">{{ $t('uploaded_studies') }}:</span>
|
||||||
|
<br />
|
||||||
|
<span v-for="(study, studyId) in report.uploadedStudies" :key="studyId">
|
||||||
|
<router-link
|
||||||
|
v-bind:to="'/filtered-studies?StudyInstanceUID=' + study.MainDicomTags['StudyInstanceUID'] + '&expand=study'"
|
||||||
|
class="upload-details-study">{{ this.getStudyLine(studyId, study.MainDicomTags,
|
||||||
|
study.PatientMainDicomTags)
|
||||||
|
}}</router-link>
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
3
WebApplication/src/globalConfigurations.js
Normal file
3
WebApplication/src/globalConfigurations.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export var baseOe2Url = window.location.pathname.slice(0, -1);
|
||||||
|
export var orthancApiUrl = '../../'
|
||||||
|
export var oe2ApiUrl = '../api/';
|
||||||
39
WebApplication/src/helpers/clipboard-helpers.js
Normal file
39
WebApplication/src/helpers/clipboard-helpers.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
export default {
|
||||||
|
copyToClipboard(text, onSuccess, onFailure) {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(text).then(
|
||||||
|
() => {
|
||||||
|
if (onSuccess !== undefined) {
|
||||||
|
onSuccess();
|
||||||
|
} else {
|
||||||
|
console.log("copied to clipboard: ", text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (onFailure !== undefined) {
|
||||||
|
onFailure();
|
||||||
|
} else {
|
||||||
|
console.log("failed to copy to clipboard")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else { // this happens, e.g in unsecure contexts (https://discourse.orthanc-server.org/t/copy-icon-oe2-problem/3958/4)
|
||||||
|
const tmp = document.createElement('TEXTAREA');
|
||||||
|
const focus = document.activeElement;
|
||||||
|
|
||||||
|
tmp.value = text;
|
||||||
|
|
||||||
|
document.body.appendChild(tmp);
|
||||||
|
tmp.select();
|
||||||
|
document.execCommand('copy'); // note: this is deprecated but we use this code only as a fallback
|
||||||
|
document.body.removeChild(tmp);
|
||||||
|
focus.focus();
|
||||||
|
|
||||||
|
if (onSuccess !== undefined) {
|
||||||
|
onSuccess();
|
||||||
|
} else {
|
||||||
|
console.log("hopefully copied to clipboard: ", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
91
WebApplication/src/helpers/date-helpers.js
Normal file
91
WebApplication/src/helpers/date-helpers.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { parse, format, endOfMonth, endOfYear, startOfMonth, startOfYear, subMonths, subDays, startOfWeek, endOfWeek, subYears } from 'date-fns';
|
||||||
|
|
||||||
|
document._datePickerPresetRanges = [
|
||||||
|
{ tLabel: 'date_picker.today', value: [new Date(), new Date()] },
|
||||||
|
{ tLabel: 'date_picker.yesterday', value: [subDays(new Date(), 1), subDays(new Date(), 1)] },
|
||||||
|
{ tLabel: 'date_picker.this_week', value: [startOfWeek(new Date(), { weekStartsOn: 1 }), new Date()] },
|
||||||
|
{ tLabel: 'date_picker.last_week', value: [startOfWeek(subDays(new Date(), 7), { weekStartsOn: 1 }), endOfWeek(subDays(new Date(), 7), { weekStartsOn: 1 })] },
|
||||||
|
{ tLabel: 'date_picker.this_month', value: [startOfMonth(new Date()), endOfMonth(new Date())] },
|
||||||
|
{ tLabel: 'date_picker.last_month', value: [startOfMonth(subMonths(new Date(), 1)), endOfMonth(subMonths(new Date(), 1))] },
|
||||||
|
{ tLabel: 'date_picker.last_12_months', value: [subYears(new Date(), 1), new Date()] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
parse(dateStr, format) {
|
||||||
|
return parse(dateStr, format, 0);
|
||||||
|
},
|
||||||
|
toDicomDate(date) {
|
||||||
|
return (date.getFullYear() * 10000 + (date.getMonth()+1) * 100 + date.getDate()).toString();
|
||||||
|
},
|
||||||
|
fromDicomDate(dateStr) {
|
||||||
|
let match = null;
|
||||||
|
match = dateStr.match(/^(\d{4})(\d{1,2})(\d{1,2})$/); // yyyymmdd (DICOM)
|
||||||
|
if (match) {
|
||||||
|
return new Date(match[1], match[2]-1, match[3]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
parseDateForDatePicker(str) {
|
||||||
|
let match = null;
|
||||||
|
match = str.match(/^(\d{4})(\d{1,2})(\d{1,2})$/); // yyyymmdd (DICOM)
|
||||||
|
if (match) {
|
||||||
|
return [new Date(match[1], match[2]-1, match[3]), null];
|
||||||
|
}
|
||||||
|
match = str.match(/^(\d{4})(\d{1,2})(\d{1,2})\-(\d{4})(\d{1,2})(\d{1,2})$/); // yyyymmdd-yyyymmdd (DICOM range)
|
||||||
|
if (match) {
|
||||||
|
return [new Date(match[1], match[2]-1, match[3]), new Date(match[4], match[5]-1, match[6])];
|
||||||
|
}
|
||||||
|
// match = str.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/); // dd/mm/yyyy or dd-mm-yyyy
|
||||||
|
// if (match) {
|
||||||
|
// return [new Date(match[3], match[2]-1, match[1]), null];
|
||||||
|
// }
|
||||||
|
// match = str.match(/^(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})$/); // yyyy/mm/dd or yyyy-mm-dd
|
||||||
|
// if (match) {
|
||||||
|
// return [new Date(match[1], match[2]-1, match[3]), null];
|
||||||
|
// }
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
dicomDateFromDatePicker(dates) {
|
||||||
|
let output = "";
|
||||||
|
if (dates == null) {
|
||||||
|
output = null;
|
||||||
|
}
|
||||||
|
else if (dates instanceof Date) {
|
||||||
|
output = this.toDicomDate(dates);
|
||||||
|
}
|
||||||
|
else if (dates instanceof Array) {
|
||||||
|
if (dates.length == 2 && (dates[0] != null && dates[0].getFullYear != undefined) && (dates[1] != null && dates[1].getFullYear != undefined)) {
|
||||||
|
if (this.toDicomDate(dates[0]) == this.toDicomDate(dates[1])) {
|
||||||
|
output = this.toDicomDate(dates[0]);
|
||||||
|
} else {
|
||||||
|
output = this.toDicomDate(dates[0]) + "-" + this.toDicomDate(dates[1]);
|
||||||
|
}
|
||||||
|
} else if (dates.length >= 1 && dates[0].getFullYear != undefined) {
|
||||||
|
output = this.toDicomDate(dates[0]);
|
||||||
|
} else if (dates.length == 2 && typeof dates[0] == 'string' && typeof dates[1] == 'string') {
|
||||||
|
output = dates[0] + "-" + dates[1];
|
||||||
|
} else if (dates.length == 1 && typeof dates[0] == 'string') {
|
||||||
|
output = dates[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
formatDateForDisplay(dicomDate, dateFormat) {
|
||||||
|
if (!dicomDate) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (dicomDate && dicomDate.length == 8) {
|
||||||
|
let d = parse(dicomDate, "yyyyMMdd", new Date());
|
||||||
|
if (!isNaN(d.getDate())) {
|
||||||
|
return format(d, dateFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dicomDate.length > 0) {
|
||||||
|
return dicomDate;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
isDateTag(tagName) {
|
||||||
|
return ["StudyDate", "PatientBirthDate", "SeriesDate", "AcquisitionDate", "ContentDate"].indexOf(tagName) != -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
WebApplication/src/helpers/label-helpers.js
Normal file
22
WebApplication/src/helpers/label-helpers.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
export default {
|
||||||
|
filterLabel(str) {
|
||||||
|
const regexLabel = new RegExp("^[0-9\-\_a-zA-Z]+$");
|
||||||
|
if (str && str.length > 0 && !regexLabel.test(str)) {
|
||||||
|
console.log("invalid label: ", str);
|
||||||
|
const invalidLabelTips = document.querySelectorAll('.invalid-label-tips');
|
||||||
|
invalidLabelTips.forEach(element => {
|
||||||
|
element.classList.remove('invalid-label-tips-hidden');
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
const invalidLabelTips = document.querySelectorAll('.invalid-label-tips');
|
||||||
|
invalidLabelTips.forEach(element => {
|
||||||
|
element.classList.add('invalid-label-tips-hidden');
|
||||||
|
})
|
||||||
|
}, 8000);
|
||||||
|
} else {
|
||||||
|
// console.log("valid '" + str + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.replace(/[^0-9\-\_a-zA-Z]/gi, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
89
WebApplication/src/helpers/resource-helpers.js
Normal file
89
WebApplication/src/helpers/resource-helpers.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import store from "../store"
|
||||||
|
import api from "../orthancApi"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getResourceTitle(resourceType, patientMainDicomTags, studyMainDicomTags, seriesMainDicomTags, instanceTags) {
|
||||||
|
let title = [];
|
||||||
|
|
||||||
|
function addIfExists(title, key, dico) {
|
||||||
|
if (key in dico) {
|
||||||
|
title.push(dico[key]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (resourceType == "study" || resourceType == "series") {
|
||||||
|
addIfExists(title, "PatientID", patientMainDicomTags)
|
||||||
|
addIfExists(title, "StudyID", studyMainDicomTags)
|
||||||
|
addIfExists(title, "StudyDate", studyMainDicomTags)
|
||||||
|
addIfExists(title, "StudyDescription", studyMainDicomTags)
|
||||||
|
}
|
||||||
|
if (resourceType == "series" || resourceType == "instance") {
|
||||||
|
addIfExists(title, "SeriesNumber", seriesMainDicomTags);
|
||||||
|
addIfExists(title, "SeriesDescription", seriesMainDicomTags);
|
||||||
|
}
|
||||||
|
if (resourceType == "instance") {
|
||||||
|
if ("0020,0013" in instanceTags) {
|
||||||
|
title.push("instance # " + instanceTags["0020,0013"]["Value"]);
|
||||||
|
} else {
|
||||||
|
title.push("instance");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return title.join(" | ");
|
||||||
|
},
|
||||||
|
|
||||||
|
patientNameCapture : "([^\\^]+)\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?\\^?([^\\^]+)?",
|
||||||
|
patientNameFormatting : null,
|
||||||
|
formatPatientName(originalPatientName) {
|
||||||
|
if (originalPatientName && this.patientNameFormatting && this.patientNameCapture) {
|
||||||
|
return originalPatientName.replace(new RegExp(this.patientNameCapture), this.patientNameFormatting);
|
||||||
|
} else {
|
||||||
|
return originalPatientName;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getPrimaryViewerUrl(level, orthancId, dicomId) {
|
||||||
|
if (store.state.configuration.uiOptions.ViewersOrdering.length > 0) {
|
||||||
|
for (let viewer of store.state.configuration.uiOptions.ViewersOrdering) {
|
||||||
|
if ((["osimis-web-viewer", "stone-webviewer", "volview", "wsi"].indexOf(viewer) != -1 && viewer in store.state.configuration.installedPlugins) ||
|
||||||
|
(viewer.startsWith("ohif") && viewer in store.state.configuration.installedPlugins) ||
|
||||||
|
(viewer.startsWith("ohif") && store.state.configuration.uiOptions.EnableOpenInOhifViewer3) ||
|
||||||
|
(viewer == "meddream" && store.state.configuration.uiOptions.EnableOpenInMedDreamViewer))
|
||||||
|
{
|
||||||
|
return this.getViewerUrl(level, orthancId, dicomId, viewer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getViewerUrl(level, orthancId, dicomId, viewer) {
|
||||||
|
if (viewer == 'osimis-web-viewer') {
|
||||||
|
return api.getOsimisViewerUrl(level, orthancId);
|
||||||
|
} else if (viewer == 'stone-webviewer') {
|
||||||
|
return api.getStoneViewerUrl(level, dicomId)
|
||||||
|
} else if (viewer == 'volview') {
|
||||||
|
return api.getVolViewUrl(level, orthancId);
|
||||||
|
} else if (viewer == 'ohif') {
|
||||||
|
if (store.state.configuration.ohifDataSource == 'dicom-web') {
|
||||||
|
return api.getOhifViewerUrlForDicomWeb('basic', dicomId);
|
||||||
|
} else {
|
||||||
|
return api.getOhifViewerUrlForDicomJson('basic', dicomId);
|
||||||
|
}
|
||||||
|
} else if (viewer == 'ohif-vr') {
|
||||||
|
return api.getOhifViewerUrl('vr');
|
||||||
|
} else if (viewer == 'ohif-tmtv') {
|
||||||
|
return api.getOhifViewerUrl('tmtv');
|
||||||
|
} else if (viewer == 'ohif-seg') {
|
||||||
|
return api.getOhifViewerUrl('seg');
|
||||||
|
} else if (viewer == 'ohif-micro') {
|
||||||
|
return api.getOhifViewerUrl('microscopy');
|
||||||
|
} else if (viewer == 'wsi') {
|
||||||
|
return api.getWsiViewerUrl(orthancId); // note: this must be a series ID !
|
||||||
|
} else if (viewer == 'meddream') {
|
||||||
|
return store.state.configuration.uiOptions.MedDreamViewerPublicRoot + "?study=" + dicomId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
WebApplication/src/helpers/source-type.js
Normal file
7
WebApplication/src/helpers/source-type.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const SourceType = Object.freeze({
|
||||||
|
LOCAL_ORTHANC: 0,
|
||||||
|
REMOTE_DICOM: 1,
|
||||||
|
REMOTE_DICOM_WEB: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SourceType;
|
||||||
213
WebApplication/src/locales/de.json
Normal file
213
WebApplication/src/locales/de.json
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
{
|
||||||
|
"all_modalities": "Alle",
|
||||||
|
"anonymize": "Anonymisieren",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"change_password": "Passwort ändern",
|
||||||
|
"close": "Schließen",
|
||||||
|
"copy_orthanc_id": "Orthanc-ID kopieren",
|
||||||
|
"date_picker": {
|
||||||
|
"last_12_months": "Letzten 12 Monate",
|
||||||
|
"last_month": "Letzten Monat",
|
||||||
|
"last_week": "Letzte Woche",
|
||||||
|
"this_month": "Diesen Monat",
|
||||||
|
"this_week": "Diese Woche",
|
||||||
|
"today": "Heute",
|
||||||
|
"yesterday": "Gestern"
|
||||||
|
},
|
||||||
|
"default": "Standard",
|
||||||
|
"delete": "Löschen",
|
||||||
|
"delete_instance_body": "Sind Sie sicher, dass Sie diese Instanz löschen möchten?<br/>Diese Aktion kann nicht rückgängig gemacht werden!",
|
||||||
|
"delete_instance_title": "Instanz löschen?",
|
||||||
|
"delete_series_body": "Sind Sie sicher, dass Sie diese Serie löschen möchten?<br/>Diese Aktion kann nicht rückgängig gemacht werden!",
|
||||||
|
"delete_series_title": "Serien löschen?",
|
||||||
|
"delete_studies_body": "Sind Sie sicher, dass Sie diese Studien löschen möchten?<br/>Diese Aktion kann nicht rückgängig gemacht werden!",
|
||||||
|
"delete_studies_title": "Studien löschen?",
|
||||||
|
"delete_study_body": "Sind Sie sicher, dass Sie diese Studie löschen möchten?<br/>Diese Aktion kann nicht rückgängig gemacht werden!",
|
||||||
|
"delete_study_title": "Studie löschen?",
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"dicom_modalities": "DICOM Modalitäten",
|
||||||
|
"dicom_tags": {
|
||||||
|
"AccessionNumber": "Zugangsnummer",
|
||||||
|
"BodyPartExamined": "Untersuchungsregion",
|
||||||
|
"InstanceNumber": "Instanznummer",
|
||||||
|
"InstitutionName": "Institutionsname",
|
||||||
|
"ModalitiesInStudy": "Modalitäten in der Studie",
|
||||||
|
"Modality": "Modalität",
|
||||||
|
"NumberOfFrames": "Anzahl der Frames",
|
||||||
|
"NumberOfStudyRelatedSeries": "Anzahl der studienbezogenen Serien",
|
||||||
|
"PatientBirthDate": "Geburtsdatum des Patienten",
|
||||||
|
"PatientID": "Patienten-ID",
|
||||||
|
"PatientName": "Patientenname",
|
||||||
|
"PatientSex": "Geschlecht des Patienten",
|
||||||
|
"ProtocolName": "Protokollname",
|
||||||
|
"ReferringPhysicianName": "Überweisender Arzt",
|
||||||
|
"RequestingPhysician": "Anfordernder Arzt",
|
||||||
|
"SOPInstanceUID": "SOP-Instanz-UID",
|
||||||
|
"SeriesDate": "Serien-Datum",
|
||||||
|
"SeriesDescription": "Serienbeschreibung",
|
||||||
|
"SeriesInstanceUID": "Serieninstanz-UID",
|
||||||
|
"SeriesNumber": "Seriennummer",
|
||||||
|
"SeriesTime": "Serienzeit",
|
||||||
|
"StudyDate": "Studiendatum",
|
||||||
|
"StudyDescription": "Studienbeschreibung",
|
||||||
|
"StudyID": "Studien-ID",
|
||||||
|
"StudyInstanceUID": "Studieninstanz-UID",
|
||||||
|
"StudyTime": "Studienzeit",
|
||||||
|
"seriesCount": "Anzahl der Serien"
|
||||||
|
},
|
||||||
|
"dicom_web_servers": "DICOM-Webserver",
|
||||||
|
"displaying_latest_studies": "Anzeige der neuesten Studien",
|
||||||
|
"download_dicom_file": "DICOM-Datei herunterladen",
|
||||||
|
"download_dicomdir": "DICOMDIR herunterladen",
|
||||||
|
"download_zip": "ZIP herunterladen",
|
||||||
|
"drop_files": "Dateien hier ablegen",
|
||||||
|
"enter_search": "Geben Sie Suchkriterien ein, um Ergebnisse anzuzeigen!",
|
||||||
|
"error": "Fehler",
|
||||||
|
"file": "Datei",
|
||||||
|
"files": "Dateien",
|
||||||
|
"frames": "Frames",
|
||||||
|
"instance": "Instanz",
|
||||||
|
"instances": "Instanzen",
|
||||||
|
"instances_number": "Anzahl der Instanzen",
|
||||||
|
"labels": {
|
||||||
|
"add_button": "Hinzufügen",
|
||||||
|
"add_labels_message_html": "<strong>Hinzufügen</strong> dieser Labels zu allen ausgewählten Ressourcen",
|
||||||
|
"add_labels_placeholder": "Labels hinzufügen, drücken Sie Enter, um ein neues Label zu erstellen",
|
||||||
|
"added_labels_message_part_1_html": "Hinzugefügte Labels ",
|
||||||
|
"added_labels_message_part_2_html": " zu <strong>{count}</strong> Studien",
|
||||||
|
"clear_all_button": "Alle entfernen",
|
||||||
|
"clear_all_labels_message_html": "<strong>Entferne alle</strong> Labels in allen ausgewählten Ressourcen",
|
||||||
|
"cleared_labels_message_part_1_html": "Entfernte Labels ",
|
||||||
|
"cleared_labels_message_part_2_html": " von <strong>{count}</strong> Studien",
|
||||||
|
"edit_labels_button": "Labels bearbeiten",
|
||||||
|
"modal_title": "Labels in ausgewählten Ressourcen bearbeiten",
|
||||||
|
"remove_button": "Entfernen",
|
||||||
|
"remove_labels_message_html": "<strong>Entfernen</strong> dieser Labels aus allen ausgewählten Ressourcen",
|
||||||
|
"remove_labels_placeholder": "Labels entfernen",
|
||||||
|
"removed_labels_message_part_1_html": "Entfernte Labels ",
|
||||||
|
"removed_labels_message_part_2_html": " von <strong>{count}</strong> Studien",
|
||||||
|
"study_details_title": "Labels"
|
||||||
|
},
|
||||||
|
"legacy_ui": "Alte Benutzeroberfläche",
|
||||||
|
"loading_latest_studies": "Laden der neuesten Studien",
|
||||||
|
"local_studies": "Alle lokalen Studien",
|
||||||
|
"logout": "Abmelden",
|
||||||
|
"mammography_plugin": "Start mammogram AI",
|
||||||
|
"modalities_in_study": "Modalitäten in der Studie",
|
||||||
|
"modify": {
|
||||||
|
"anonymize_button_title": "Anonymisieren",
|
||||||
|
"anonymize_modal_title": "Anonymisieren",
|
||||||
|
"autogenerated_dicom_uid": "Automatisch generiert",
|
||||||
|
"back_button_title": "Zurück",
|
||||||
|
"error_attach_series_to_existing_study_target_does_not_exist_html": "Fehler beim Ändern der Serie, keine Studie mit dieser Studieninstanz-UID gefunden.",
|
||||||
|
"error_attach_series_to_existing_study_target_unchanged_html": "Fehler beim Ändern der Serie, die Zielstudie ist dieselbe wie die aktuelle Serienstudie.",
|
||||||
|
"error_attach_study_to_existing_patient_target_does_not_exist_html": "Fehler beim Ändern des Patienten, kein Patient mit dieser Patienten-ID gefunden. Bitte verwenden Sie die <strong>Ändern von Studien-Tags</strong> Funktion.",
|
||||||
|
"error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Fehler beim Ändern von Studien-Tags: Dieser Patient hat bereits andere Studien. Bitte verwenden Sie die <strong>Patient-Tags ändern</strong> Funktion.",
|
||||||
|
"error_modify_any_study_tags_patient_exists_html": "Fehler beim Ändern von Studien-Tags: Ein anderer Patient mit derselben Patienten-ID existiert bereits. Bitte verwenden Sie die <strong>Patient ändern</strong> Funktion.",
|
||||||
|
"error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Fehler beim Ändern von Patienten-Tags: Ein anderer Patient mit derselben Patienten-ID existiert bereits. Bitte verwenden Sie die <strong>Patient-Tags ändern</strong> Funktion.",
|
||||||
|
"error_modify_unexpected_error_html": "Unerwarteter Fehler während der Änderung, überprüfen Sie die Orthanc-Protokolle",
|
||||||
|
"insert_tag": "DICOM-Tag einfügen",
|
||||||
|
"job_in_progress": "Änderung wird verarbeitet",
|
||||||
|
"modify_button_title": "Ändern",
|
||||||
|
"modify_modal_title": "Ändern",
|
||||||
|
"modify_study_mode_duplicate_html": "Erstellen Sie eine Kopie der Originalstudie mit Ihren Änderungen. <small>(generiert neue DICOM-UIDs und behält die Originalstudie)</small>",
|
||||||
|
"modify_study_mode_keep_uids_html": "Originalstudie verändern. <small>(behalten die originalen DICOM-UIDs)</small>",
|
||||||
|
"modify_study_mode_new_uids_html": "Originalstudie verändern. <small>(generiert neue DICOM-UIDs)</small>",
|
||||||
|
"remove_tag_tooltip": "Tag entfernen",
|
||||||
|
"remove_tag_undo_tooltip": "Tag entfernen rückgängig machen",
|
||||||
|
"series_step_0_attach_series_to_existing_study_button_title_html": "Studie ändern",
|
||||||
|
"series_step_0_attach_series_to_existing_study_html": "Diese Serie zu einer vorhandenen Studie verschieben.",
|
||||||
|
"series_step_0_create_new_study_button_title_html": "Neue Studie erstellen",
|
||||||
|
"series_step_0_create_new_study_html": "Diese Serie in eine neue Studie verschieben.",
|
||||||
|
"series_step_0_modify_series_button_title_html": "Seriendaten ändern",
|
||||||
|
"series_step_0_modify_series_html": "Seriendaten in dieser Serie ändern?",
|
||||||
|
"show_modified_resources": "Modifizierte Ressourcen anzeigen",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_button_title_html": "Patient ändern",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_html": "Diese Studie an einen anderen vorhandenen Patienten anhängen.",
|
||||||
|
"study_step_0_modify_study_button_title_html": "Studien-Tags ändern",
|
||||||
|
"study_step_0_modify_study_html": "Patienten-/Studien-Tags in dieser Studie ändern?",
|
||||||
|
"study_step_0_patient_has_other_studies_button_title_html": "Patienten-Tags ändern",
|
||||||
|
"study_step_0_patient_has_other_studies_html": "Dieser Patient hat insgesamt <strong>{count}</strong> Studien.<br/>Möchten Sie den Patienten in all diesen Studien ändern?",
|
||||||
|
"warning_create_new_study_from_series_html": "Ein Patient mit derselben Patienten-ID existiert bereits mit unterschiedlichen Tags. Möchten Sie den vorhandenen Patientennamen, das Geschlecht und das Geburtsdatum verwenden?"
|
||||||
|
},
|
||||||
|
"my_jobs": "Meine Aufträge",
|
||||||
|
"no_modalities": "Keine",
|
||||||
|
"no_result_found": "Kein Ergebnis gefunden!",
|
||||||
|
"not_showing_all_results": "Es können nicht alle Ergebnisse angezeigt werden. Sie sollten Ihre Suchkriterien verfeinern",
|
||||||
|
"open": "Öffnen",
|
||||||
|
"page_not_found": "Seite nicht gefunden!",
|
||||||
|
"plugin": "Plugin",
|
||||||
|
"preview": "Vorschau",
|
||||||
|
"profile": "Profil",
|
||||||
|
"retrieve_and_view": {
|
||||||
|
"finding_locally": "Überprüfen, ob die Studie bereits lokal verfügbar ist.",
|
||||||
|
"finding_remotely": "Suchen der Studie in Remote-DICOM-Knoten.",
|
||||||
|
"not_found": "Die Studie wurde weder lokal noch in Remote-DICOM-Knoten gefunden.",
|
||||||
|
"retrieved_html": "Abgerufene <strong>{count}</strong> Instanzen.",
|
||||||
|
"retrieving": "Studie abrufen."
|
||||||
|
},
|
||||||
|
"searching": "Suchen...",
|
||||||
|
"select_files": "Dateien auswählen",
|
||||||
|
"select_folder": "Ordner auswählen",
|
||||||
|
"send_to": {
|
||||||
|
"button_title": "Senden",
|
||||||
|
"dicom": "An DICOM-Knoten senden",
|
||||||
|
"dicom_web": "An DICOMWeb-Server senden",
|
||||||
|
"orthanc_peer": "An Orthanc Peer senden",
|
||||||
|
"transfers": "An Orthanc Peer senden (erweiterte Übertragungen)"
|
||||||
|
},
|
||||||
|
"series": "Serie",
|
||||||
|
"series_count_header": "# Serien",
|
||||||
|
"series_plural": "Serien",
|
||||||
|
"settings": {
|
||||||
|
"dicom_AET": "DICOM AET",
|
||||||
|
"dicom_port": "DICOM-Port",
|
||||||
|
"ingest_transcoding": "Transcoding übernehmen",
|
||||||
|
"installed_plugins": "Installierte Plugins",
|
||||||
|
"orthanc_name": "Orthanc-Name",
|
||||||
|
"orthanc_system_info": "Orthanc-Systeminformationen",
|
||||||
|
"orthanc_version": "Orthanc-Version",
|
||||||
|
"overwrite_instances": "Instanzen überschreiben",
|
||||||
|
"plugins_not_enabled": "Geladene, aber nicht aktivierte oder nicht korrekt konfigurierte Plugins sind ",
|
||||||
|
"statistics": "Statistiken",
|
||||||
|
"storage_compression": "Speicherkompression",
|
||||||
|
"storage_size": "Speichergröße",
|
||||||
|
"striked_through": "durchgestrichen",
|
||||||
|
"title": "Einstellungen",
|
||||||
|
"verbosity_level": "Ausführlichkeitsstufe"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"anonymize": "Anonymisieren?",
|
||||||
|
"button_title": "Teilen",
|
||||||
|
"copy_and_close": "Kopieren und Schließen",
|
||||||
|
"days": "Tage",
|
||||||
|
"expires_in": "Läuft ab in",
|
||||||
|
"link": "Link",
|
||||||
|
"modal_title": "Studie teilen",
|
||||||
|
"never": "niemals"
|
||||||
|
},
|
||||||
|
"show_errors": "Fehler anzeigen",
|
||||||
|
"studies": "Studien",
|
||||||
|
"study": "Studie",
|
||||||
|
"this_patient_has_no_other_studies": "Dieser Patient hat keine anderen Studien.",
|
||||||
|
"this_patient_has_other_studies": "Dieser Patient hat insgesamt {count} Studien.",
|
||||||
|
"this_patient_has_other_studies_show": "Andere Studien anzeigen!",
|
||||||
|
"token": {
|
||||||
|
"error_token_expired_html": "Ihr Token ist abgelaufen.",
|
||||||
|
"error_token_invalid_html": "Ihr Token ist ungültig.<br/>Überprüfen Sie, ob Sie es vollständig eingefügt haben, oder kontaktieren Sie die Person, die es Ihnen zur Verfügung gestellt hat.",
|
||||||
|
"error_token_unknown_html": "Ihr Token ist ungültig.<br/>Überprüfen Sie, ob Sie es vollständig eingefügt haben, oder kontaktieren Sie die Person, die es Ihnen zur Verfügung gestellt hat.",
|
||||||
|
"token_being_checked_html": "Ihr Token wird überprüft."
|
||||||
|
},
|
||||||
|
"trace": "Trace",
|
||||||
|
"upload": "Hochladen",
|
||||||
|
"uploaded_studies": "Hochgeladene Studien",
|
||||||
|
"verbose": "Ausführlich",
|
||||||
|
"view_in_meddream": "In MedDream anzeigen",
|
||||||
|
"view_in_ohif": "In OHIF anzeigen",
|
||||||
|
"view_in_ohif_tmtv": "In OHIF Total Metabolic Tumor Volume-Modus anzeigen",
|
||||||
|
"view_in_ohif_vr": "In OHIF Volumenrendering-Modus anzeigen",
|
||||||
|
"view_in_osimis": "In OsimisViewer anzeigen",
|
||||||
|
"view_in_stone": "In StoneViewer anzeigen",
|
||||||
|
"view_in_volview": "In VolView anzeigen",
|
||||||
|
"view_in_wsi_viewer": "In Whole Slide Imaging Viewer anzeigen"
|
||||||
|
}
|
||||||
270
WebApplication/src/locales/en.json
Normal file
270
WebApplication/src/locales/en.json
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
{
|
||||||
|
"add_series": {
|
||||||
|
"button_title": "Add a series",
|
||||||
|
"drop_file": "Drop a single PDF, image or STL file here or",
|
||||||
|
"error_add_series_unexpected_error_html": "Unexpected error while adding a series, check Orthanc logs",
|
||||||
|
"modal_title": "Add a new series to study",
|
||||||
|
"select_file": "Select a file",
|
||||||
|
"unrecognized_file_format": "Unrecognized file format. Only PDF, image files and STL files are supported",
|
||||||
|
"uploaded_file": "The file {name} ({size}) is of {type} type."
|
||||||
|
},
|
||||||
|
"all_modalities": "All",
|
||||||
|
"anonymize": "Anonymize",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"change_password": "Change password",
|
||||||
|
"close": "Close",
|
||||||
|
"copy_orthanc_id": "Copy Orthanc ID",
|
||||||
|
"date_picker": {
|
||||||
|
"last_12_months": "Last 12 months",
|
||||||
|
"last_month": "Last month",
|
||||||
|
"last_week": "Last week",
|
||||||
|
"this_month": "This month",
|
||||||
|
"this_week": "This week",
|
||||||
|
"today": "Today",
|
||||||
|
"yesterday": "Yesterday"
|
||||||
|
},
|
||||||
|
"default": "Default",
|
||||||
|
"delete": "Delete",
|
||||||
|
"delete_instance_body": "Are you sure you want to delete this instance?<br/>This action can not be undone!",
|
||||||
|
"delete_instance_title": "Delete instance?",
|
||||||
|
"delete_series_body": "Are you sure you want to delete this series?<br/>This action can not be undone!",
|
||||||
|
"delete_series_title": "Delete series?",
|
||||||
|
"delete_studies_body": "Are you sure you want to delete these studies?<br/>This action can not be undone!",
|
||||||
|
"delete_studies_title": "Delete studies?",
|
||||||
|
"delete_study_body": "Are you sure you want to delete this study?<br/>This action can not be undone!",
|
||||||
|
"delete_study_title": "Delete study?",
|
||||||
|
"description": "Description",
|
||||||
|
"dicom_modalities": "DICOM Modalities",
|
||||||
|
"dicom_tags": {
|
||||||
|
"AccessionNumber": "Accession number",
|
||||||
|
"BodyPartExamined": "Body part examined",
|
||||||
|
"InstanceNumber": "Instance number",
|
||||||
|
"InstitutionName": "Institution Name",
|
||||||
|
"ModalitiesInStudy": "Modalities in Study",
|
||||||
|
"Modality": "Modality",
|
||||||
|
"NumberOfFrames": "Number of frames",
|
||||||
|
"NumberOfStudyRelatedSeries": "Number of series",
|
||||||
|
"OtherPatientIDs": "Patient Other IDs",
|
||||||
|
"PatientBirthDate": "Patient Birth Date",
|
||||||
|
"PatientID": "Patient ID",
|
||||||
|
"PatientName": "Patient Name",
|
||||||
|
"PatientSex": "Patient sex",
|
||||||
|
"ProtocolName": "Protocol Name",
|
||||||
|
"ReferringPhysicianName": "Referring Physician Name",
|
||||||
|
"RequestingPhysician": "Requesting Physician",
|
||||||
|
"SOPInstanceUID": "SOP Instance UID",
|
||||||
|
"SeriesDate": "Series Date",
|
||||||
|
"SeriesDescription": "Series Description",
|
||||||
|
"SeriesInstanceUID": "Series Instance UID",
|
||||||
|
"SeriesNumber": "Series number",
|
||||||
|
"SeriesTime": "Series Time",
|
||||||
|
"StudyDate": "Study Date",
|
||||||
|
"StudyDescription": "Study Description",
|
||||||
|
"StudyID": "Study ID",
|
||||||
|
"StudyInstanceUID": "Study Instance UID",
|
||||||
|
"StudyTime": "Study Time",
|
||||||
|
"seriesCount": "Number of series"
|
||||||
|
},
|
||||||
|
"dicom_web_servers": "DICOM-Web Servers",
|
||||||
|
"displaying_latest_studies": "Displaying most recent studies",
|
||||||
|
"download_dicom_file": "Download DICOM file",
|
||||||
|
"download_dicomdir": "Download DICOMDIR",
|
||||||
|
"download_zip": "Download ZIP",
|
||||||
|
"drop_files": "Drop files here or",
|
||||||
|
"enter_search": "Enter a search criteria to show results!",
|
||||||
|
"error": "Error",
|
||||||
|
"file": "File",
|
||||||
|
"files": "files",
|
||||||
|
"frames": "frames",
|
||||||
|
"instance": "Instance",
|
||||||
|
"instances": "Instances",
|
||||||
|
"instances_count_header": "# instances",
|
||||||
|
"instances_number": "Number of instances",
|
||||||
|
"job_success": "Success",
|
||||||
|
"job_failure": "Failure",
|
||||||
|
"labels": {
|
||||||
|
"add_button": "Add",
|
||||||
|
"add_labels_message_html": "<strong>Add</strong> these labels to all selected resources",
|
||||||
|
"add_labels_placeholder": "Labels to add, press Enter to create or add a new one",
|
||||||
|
"add_labels_placeholder_no_create": "Labels to add",
|
||||||
|
"added_labels_message_part_1_html": "Added labels ",
|
||||||
|
"added_labels_message_part_2_html": " to <strong>{count}</strong> studies",
|
||||||
|
"clear_all_button": "Remove all",
|
||||||
|
"clear_all_labels_message_html": "<strong>Remove all</strong> labels in all selected resources",
|
||||||
|
"cleared_labels_message_part_1_html": "Removed labels ",
|
||||||
|
"cleared_labels_message_part_2_html": " from <strong>{count}</strong> studies",
|
||||||
|
"edit_labels_button": "Edit labels",
|
||||||
|
"modal_title": "Modify labels in selected resources",
|
||||||
|
"remove_button": "Remove",
|
||||||
|
"remove_labels_message_html": "<strong>Remove</strong> these labels from all selected resources",
|
||||||
|
"remove_labels_placeholder": "Labels to remove",
|
||||||
|
"removed_labels_message_part_1_html": "Removed labels ",
|
||||||
|
"removed_labels_message_part_2_html": " from <strong>{count}</strong> studies",
|
||||||
|
"study_details_title": "Labels",
|
||||||
|
"valid_alphabet_warning": "Only allowed characters in labels are: latin letters, numbers, - and _"
|
||||||
|
},
|
||||||
|
"legacy_ui": "Legacy UI",
|
||||||
|
"loading_latest_studies": "Loading most recent studies",
|
||||||
|
"local_studies": "All local Studies",
|
||||||
|
"logout": "Logout",
|
||||||
|
"mammography_plugin": "Start mammography AI",
|
||||||
|
"modalities_in_study": "Modalities in Study",
|
||||||
|
"modify": {
|
||||||
|
"anonymize_button_title": "Anonymize",
|
||||||
|
"anonymize_modal_title": "Anonymize",
|
||||||
|
"autogenerated_dicom_uid": "Auto-generated",
|
||||||
|
"back_button_title": "Back",
|
||||||
|
"error_attach_series_to_existing_study_target_does_not_exist_html": "Error while changing series, no study found with this StudyInstanceUID.",
|
||||||
|
"error_attach_series_to_existing_study_target_unchanged_html": "Error while changing series, the target study is the same as the current series study.",
|
||||||
|
"error_attach_study_to_existing_patient_target_does_not_exist_html": "Error while changing patient, no patient found with this PatientID. You should probably use the <strong>Modify Study tags</strong> function instead.",
|
||||||
|
"error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Error while modifyting study tags: this patient has other studies. You should use the <strong>Modify Patient tags</strong> function instead.",
|
||||||
|
"error_modify_any_study_tags_patient_exists_html": "Error while modifying study tags: another patient with the same PatientID already exists. You should use the <strong>Change patient</strong> function instead.",
|
||||||
|
"error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Error while modifying a patient tags: another patient with the same PatientID already exists. You should use the <strong>Modify Patient tags</strong> function instead.",
|
||||||
|
"error_modify_unexpected_error_html": "Unexpected error during modification, check Orthanc logs",
|
||||||
|
"insert_tag": "Insert a DICOM Tag",
|
||||||
|
"job_in_progress": "Modification is being processed",
|
||||||
|
"modify_button_title": "Modify",
|
||||||
|
"modify_modal_title": "Modify",
|
||||||
|
"modify_series_mode_duplicate_html": "Create a modified copy of the original series. <small>(generating new DICOM UIDs and keeping the original series)</small>",
|
||||||
|
"modify_series_mode_keep_uids_html": "Modify the original series. <small>(keeping the original DICOM UIDs)</small>",
|
||||||
|
"modify_series_mode_new_uids_html": "Modify the original series. <small>(generating new DICOM UIDs)</small>",
|
||||||
|
"modify_study_mode_duplicate_html": "Create a modified copy of the original study. <small>(generating new DICOM UIDs and keeping the original study)</small>",
|
||||||
|
"modify_study_mode_keep_uids_html": "Modify the original study. <small>(keeping the original DICOM UIDs)</small>",
|
||||||
|
"modify_study_mode_new_uids_html": "Modify the original study. <small>(generating new DICOM UIDs)</small>",
|
||||||
|
"remove_tag_tooltip": "Remove tag",
|
||||||
|
"remove_tag_undo_tooltip": "Undo remove tag",
|
||||||
|
"series_step_0_attach_series_to_existing_study_button_title_html": "Change study",
|
||||||
|
"series_step_0_attach_series_to_existing_study_html": "Move this series to an existing Study.",
|
||||||
|
"series_step_0_create_new_study_button_title_html": "Create new study",
|
||||||
|
"series_step_0_create_new_study_html": "Move this series to a new study.",
|
||||||
|
"series_step_0_modify_series_button_title_html": "Modify series tags",
|
||||||
|
"series_step_0_modify_series_html": "Modify Series tags in this series only?",
|
||||||
|
"show_modified_resources": "Show modified resources",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_button_title_html": "Change patient",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_html": "Attach this study to another existing Patient.",
|
||||||
|
"study_step_0_modify_study_button_title_html": "Modify Study tags",
|
||||||
|
"study_step_0_modify_study_html": "Modify Patient/Study tags in this study only?",
|
||||||
|
"study_step_0_patient_has_other_studies_button_title_html": "Modify Patient tags",
|
||||||
|
"study_step_0_patient_has_other_studies_html": "This patient has <strong>{count}</strong> studies in total.<br/>Do you want to modify the patient in all these studies?",
|
||||||
|
"warning_create_new_study_from_series_html": "A patient with the same PatientID already exists with differing tags. Do you want to use the existing PatientName, PatientSex and PatientBirthDate?"
|
||||||
|
},
|
||||||
|
"my_jobs": "My jobs",
|
||||||
|
"no_modalities": "None",
|
||||||
|
"no_result_found": "No result found!",
|
||||||
|
"not_showing_all_results": "Not showing all results. You should refine your search criteria",
|
||||||
|
"open": "Open",
|
||||||
|
"page_not_found": "Page not found!",
|
||||||
|
"patients": "Patients",
|
||||||
|
"plugin": "plugin",
|
||||||
|
"plugins": {
|
||||||
|
"delayed_deletion": {
|
||||||
|
"pending_files_count": "Delayed Deletion plugin: Files to delete"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preview": "Preview",
|
||||||
|
"profile": "Profile",
|
||||||
|
"retrieve": "Retrieve",
|
||||||
|
"retrieve_and_view": {
|
||||||
|
"finding_locally": "Checking if the study is already available locally.",
|
||||||
|
"finding_remotely": "Searching for the study in remote DICOM nodes.",
|
||||||
|
"not_found": "The study was not found neither locally nor in remote DICOM nodes.",
|
||||||
|
"retrieved_html": "Retrieved <strong>{count}</strong> instances.",
|
||||||
|
"retrieving": "Retrieving study."
|
||||||
|
},
|
||||||
|
"remote_dicom_browsing": "You are currently browsing the <strong>{source}</strong> remote <strong>DICOM</strong> node",
|
||||||
|
"remote_dicom_web_browsing": "You are currently browsing the <strong>{source}</strong> remote <strong>DICOMWeb</strong> server",
|
||||||
|
"save": "Save",
|
||||||
|
"searching": "Searching...",
|
||||||
|
"select_files": "Select Files",
|
||||||
|
"select_folder": "Select Folder",
|
||||||
|
"send_to": {
|
||||||
|
"button_title": "Send",
|
||||||
|
"dicom": "Send to DICOM node",
|
||||||
|
"dicom_web": "Send to DICOMWeb server",
|
||||||
|
"orthanc_peer": "Send to Orthanc Peer",
|
||||||
|
"transfers": "Send to Orthanc Peer (advanced-transfers)"
|
||||||
|
},
|
||||||
|
"series": "Series",
|
||||||
|
"series_count_header": "# series",
|
||||||
|
"series_and_instances_count_header": "# Ser/Inst",
|
||||||
|
"series_plural": "Series",
|
||||||
|
"settings": {
|
||||||
|
"allow_all_currents_and_futures_labels": "All current and future labels",
|
||||||
|
"available_labels_global_instructions_html": "Since you have enabled permissions and you are an administrator, this page enables you to create/delete the available labels. Users won't be able to create other labels.<br/>Once you have updated this list, don't forget to click <strong>Save</strong> to apply your changes.",
|
||||||
|
"available_labels_title": "Available labels",
|
||||||
|
"create_new_label": "Create new label",
|
||||||
|
"dicom_AET": "DICOM AET",
|
||||||
|
"dicom_port": "DICOM Port",
|
||||||
|
"ingest_transcoding": "Ingest transcoding",
|
||||||
|
"installed_plugins": "Installed Plugins",
|
||||||
|
"labels_clear_selection": "Clear selection",
|
||||||
|
"labels_description_html": "Labels are created/deleted in the settings/Labels page.",
|
||||||
|
"labels_select_all_currents": "Select all currently defined labels",
|
||||||
|
"labels_limit_available_labels": "Limit available labels to a list",
|
||||||
|
"labels_list_empty": "The labels list is currently empty",
|
||||||
|
"labels_based_permissions_title": "Labels based permissions",
|
||||||
|
"labels_title": "Labels",
|
||||||
|
"global_permissions_title": "Global permissions",
|
||||||
|
"orthanc_name": "Orthanc Name",
|
||||||
|
"orthanc_system_info": "Orthanc System Info",
|
||||||
|
"orthanc_version": "Orthanc Version",
|
||||||
|
"overwrite_instances": "Overwrite instances",
|
||||||
|
"permissions": "Permissions",
|
||||||
|
"permissions_clear_selection": "Clear selection",
|
||||||
|
"permissions_global_instructions_html": "Edit the permissions below and click <strong>Save</strong> to apply your modifications.<br>For each role, select the labels whose access is allowed and the allowed permissions.",
|
||||||
|
"permissions_instructions": "The permissions are statically defined in the authorization-plugin configuration.",
|
||||||
|
"read_only": "Read only system",
|
||||||
|
"select_all_label_permissions": "Select all label permissions",
|
||||||
|
"select_all_global_permissions": "Select all global permissions",
|
||||||
|
"plugins_not_enabled": "Plugins that are loaded but not enabled or not configured correctly are ",
|
||||||
|
"roles_title": "Roles",
|
||||||
|
"roles_description_html": "The roles are created/deleted in Keycloak",
|
||||||
|
"statistics": "Statistics",
|
||||||
|
"storage_compression": "Storage Compression",
|
||||||
|
"storage_size": "Storage Size",
|
||||||
|
"striked_through": "striked-through",
|
||||||
|
"system_info": "System info",
|
||||||
|
"title": "Settings",
|
||||||
|
"verbosity_level": "Verbosity Level"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"anonymize": "Anonymize?",
|
||||||
|
"button_title": "Share",
|
||||||
|
"copy_and_close": "Copy and close",
|
||||||
|
"days": "days",
|
||||||
|
"expires_in": "Expires in",
|
||||||
|
"link": "Link",
|
||||||
|
"modal_title": "Share study",
|
||||||
|
"modal_title_multiple_studies": "Share {count} studies",
|
||||||
|
"never": "never"
|
||||||
|
},
|
||||||
|
"show_errors": "Show errors",
|
||||||
|
"studies": "Studies",
|
||||||
|
"study": "Study",
|
||||||
|
"this_patient_has_no_other_studies": "This patient has no other studies.",
|
||||||
|
"this_patient_has_other_studies": "This patient has {count} studies in total.",
|
||||||
|
"this_patient_has_other_studies_show": "Show them!",
|
||||||
|
"this_remote_patient_has_local_studies": "This patient has {count} studies already stored locally.",
|
||||||
|
"this_study_is_already_stored_locally": "This study is already stored locally.",
|
||||||
|
"this_study_is_already_stored_locally_show": "Show it!",
|
||||||
|
"token": {
|
||||||
|
"error_token_expired_html": "Your token has expired.",
|
||||||
|
"error_token_invalid_html": "Your token is invalid.<br/>Check if you have pasted it completely, or contact the person who has provided it to you.",
|
||||||
|
"error_token_unknown_html": "Your token is invalid.<br/>Check if you have pasted it completely, or contact the person who has provided it to you.",
|
||||||
|
"token_being_checked_html": "Your token is being checked."
|
||||||
|
},
|
||||||
|
"trace": "Trace",
|
||||||
|
"upload": "Upload",
|
||||||
|
"uploaded_studies": "Uploaded studies",
|
||||||
|
"verbose": "Verbose",
|
||||||
|
"view_in_meddream": "View in MedDream",
|
||||||
|
"view_in_ohif": "View in OHIF",
|
||||||
|
"view_in_ohif_microscopy": "View in OHIF Microscopy mode",
|
||||||
|
"view_in_ohif_seg": "View in OHIF Segmentation mode",
|
||||||
|
"view_in_ohif_tmtv": "View in OHIF Total Metabolic Tumor Volume mode",
|
||||||
|
"view_in_ohif_vr": "View in OHIF Volume Rendering mode",
|
||||||
|
"view_in_osimis": "View in OsimisViewer",
|
||||||
|
"view_in_stone": "View in StoneViewer",
|
||||||
|
"view_in_volview": "View in VolView",
|
||||||
|
"view_in_wsi_viewer": "View in Whole Slide Imaging Viewer"
|
||||||
|
}
|
||||||
190
WebApplication/src/locales/es.json
Normal file
190
WebApplication/src/locales/es.json
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
{
|
||||||
|
"all_modalities": "Todos",
|
||||||
|
"anonymize": "Anonimizar",
|
||||||
|
"cancel": "Cancelar",
|
||||||
|
"change_password": "Cambiar la contraseña",
|
||||||
|
"close": "Cerrar",
|
||||||
|
"copy_orthanc_id": "Copiar Orthanc ID",
|
||||||
|
"date_picker": {
|
||||||
|
"last_12_months": "Últimos 12 meses",
|
||||||
|
"last_month": "El mes pasado",
|
||||||
|
"last_week": "La semana pasada",
|
||||||
|
"this_month": "Este mes",
|
||||||
|
"this_week": "Este semana",
|
||||||
|
"today": "Hoy",
|
||||||
|
"yesterday": "Ayer"
|
||||||
|
},
|
||||||
|
"default": "Predeterminado",
|
||||||
|
"delete": "Eliminar",
|
||||||
|
"delete_instance_body": "¿Estás seguro de que quieres eliminar este elemento?<br/>Esta acción no se puede deshacer!",
|
||||||
|
"delete_instance_title": "¿Eliminar elemento?",
|
||||||
|
"delete_series_body": "¿Estás seguro de que quieres eliminar esta serie?<br/>Esta acción no se puede deshacer!",
|
||||||
|
"delete_series_title": "¿Eliminar serie?",
|
||||||
|
"delete_studies_body": "¿Estás seguro de que quieres eliminar estos estudios?<br/>Esta acción no se puede deshacer!",
|
||||||
|
"delete_studies_title": "¿Eliminar estudios?",
|
||||||
|
"delete_study_body": "¿Estás seguro de que quieres eliminar este estudio?<br/>Esta acción no se puede deshacer!",
|
||||||
|
"delete_study_title": "¿Eliminar estudio?",
|
||||||
|
"description": "Descripción",
|
||||||
|
"dicom_modalities": "Modalidades DICOM",
|
||||||
|
"dicom_tags": {
|
||||||
|
"AccessionNumber": "Número de acceso",
|
||||||
|
"BodyPartExamined": "Parte del cuerpo examinada",
|
||||||
|
"InstanceNumber": "Número de elemento",
|
||||||
|
"InstitutionName": "Nombre de la institucion",
|
||||||
|
"ModalitiesInStudy": "Modalidades en Estudio",
|
||||||
|
"Modality": "Modalidad",
|
||||||
|
"NumberOfFrames": "Número de cuadros",
|
||||||
|
"NumberOfStudyRelatedSeries": "Recuento de serie",
|
||||||
|
"PatientBirthDate": "Fecha de nacimiento del paciente",
|
||||||
|
"PatientID": "ID del paciente",
|
||||||
|
"PatientName": "Nombre del paciente",
|
||||||
|
"PatientSex": "Sexo del paciente",
|
||||||
|
"ProtocolName": "Nombre del protocolo",
|
||||||
|
"ReferringPhysicianName": "Nombre del médico de referencia",
|
||||||
|
"RequestingPhysician": "Médico solicitante",
|
||||||
|
"SeriesDate": "Fecha de la serie",
|
||||||
|
"SeriesDescription": "Descripción de la serie",
|
||||||
|
"SeriesInstanceUID": "Series Instance UID",
|
||||||
|
"SeriesNumber": "Número de serie",
|
||||||
|
"SeriesTime": "Hora de la serie",
|
||||||
|
"StudyDate": "Fecha del estudio",
|
||||||
|
"StudyDescription": "Descripción del estudio",
|
||||||
|
"StudyID": "Identificación del estudio",
|
||||||
|
"StudyInstanceUID": "Study Instance UID",
|
||||||
|
"StudyTime": "Hora de estudio",
|
||||||
|
"seriesCount": "Recuento de serie"
|
||||||
|
},
|
||||||
|
"dicom_web_servers": "Servidores DICOM-Web",
|
||||||
|
"displaying_latest_studies": "Se muestran los estudios más recientes",
|
||||||
|
"download_dicom_file": "Descargar archivo DICOM",
|
||||||
|
"download_dicomdir": "Descargar DICOMDIR",
|
||||||
|
"download_zip": "Descargar ZIP",
|
||||||
|
"drop_files": "Soltar archivos aquí o",
|
||||||
|
"enter_search": "¡Ingrese un criterio de búsqueda para mostrar resultados!",
|
||||||
|
"error": "Error",
|
||||||
|
"file": "Archivo",
|
||||||
|
"files": "archivos",
|
||||||
|
"frames": "cuadros",
|
||||||
|
"instance": "Elemento",
|
||||||
|
"instances": "Elementos",
|
||||||
|
"instances_number": "Número de elementos",
|
||||||
|
"legacy_ui": "IU heredada",
|
||||||
|
"loading_latest_studies": "Cargando estudios más recientes",
|
||||||
|
"local_studies": "Estudios locales",
|
||||||
|
"logout": "Cerrar sesión",
|
||||||
|
"mammography_plugin": "Start mammogram AI",
|
||||||
|
"modalities_in_study": "Modalidades en Estudio",
|
||||||
|
"modify": {
|
||||||
|
"anonymize_button_title": "Anonimizar",
|
||||||
|
"anonymize_modal_title": "Anonimizar",
|
||||||
|
"autogenerated_dicom_uid": "Auto generado",
|
||||||
|
"back_button_title": "Atrás",
|
||||||
|
"error_attach_series_to_existing_study_target_does_not_exist_html": "Error al cambiar de serie, no se encontró ningún estudio con este StudyInstanceUID.",
|
||||||
|
"error_attach_series_to_existing_study_target_unchanged_html": "Error al cambiar de serie, el estudio de destino es el mismo que el estudio de la serie actual.",
|
||||||
|
"error_attach_study_to_existing_patient_target_does_not_exist_html": "Error al cambiar de paciente, no se encontró ningún paciente con este ID de paciente. <strong>Modificar etiquetas de estudio</strong> función en su lugar.",
|
||||||
|
"error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Error al modificar etiquetas de estudio: este paciente tiene otros estudios. <strong>Modificar etiquetas de pacientes</strong> función en su lugar.",
|
||||||
|
"error_modify_any_study_tags_patient_exists_html": "Error al modificar las etiquetas del estudio: ya existe otro paciente con el mismo ID de paciente. <strong>Cambiar de paciente</strong> función en su lugar.",
|
||||||
|
"error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Error al modificar las etiquetas de un paciente: ya existe otro paciente con el mismo ID de paciente. <strong>Modificar etiquetas de pacientes</strong> función en su lugar.",
|
||||||
|
"error_modify_unexpected_error_html": "Error inesperado durante la modificación, verifique los registros de Orthanc",
|
||||||
|
"insert_tag": "Insertar una etiqueta DICOM",
|
||||||
|
"job_in_progress": "Se está procesando la modificación",
|
||||||
|
"modify_button_title": "Modificar",
|
||||||
|
"modify_modal_title": "Modificar",
|
||||||
|
"modify_study_mode_duplicate_html": "Crear una copia modificada del estudio original. <small>(generando nuevos DICOM UIDs y manteniendo el estudio original)</small>",
|
||||||
|
"modify_study_mode_keep_uids_html": "Modificar el estudio original. <small>(manteniendo los DICOM UIDs originales)</small>",
|
||||||
|
"modify_study_mode_new_uids_html": "Modificar el estudio original. <small>(generando nuevos DICOM UIDs)</small>",
|
||||||
|
"remove_tag_tooltip": "Eliminar etiqueta",
|
||||||
|
"remove_tag_undo_tooltip": "Deshacer eliminar etiqueta",
|
||||||
|
"series_step_0_attach_series_to_existing_study_button_title_html": "Cambiar de estudio",
|
||||||
|
"series_step_0_attach_series_to_existing_study_html": "Mover esta serie a un estudio existente.",
|
||||||
|
"series_step_0_create_new_study_button_title_html": "Crear nuevo estudio",
|
||||||
|
"series_step_0_create_new_study_html": "Mover esta serie a un nuevo estudio.",
|
||||||
|
"series_step_0_modify_series_button_title_html": "Modificar etiquetas de serie",
|
||||||
|
"series_step_0_modify_series_html": "¿Modificar etiquetas de serie solo en esta serie?",
|
||||||
|
"show_modified_resources": "Mostrar recursos modificados",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_button_title_html": "Cambiar de paciente",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_html": "Adjuntar este estudio a otro paciente existente.",
|
||||||
|
"study_step_0_modify_study_button_title_html": "Modificar etiquetas de estudio",
|
||||||
|
"study_step_0_modify_study_html": "¿Modificar etiquetas de paciente/estudio solo en este estudio?",
|
||||||
|
"study_step_0_patient_has_other_studies_button_title_html": "Modificar etiquetas de pacientes",
|
||||||
|
"study_step_0_patient_has_other_studies_html": "Este paciente tiene <strong>{count}</strong> estudios en total.<br/>¿Quieres modificar al paciente en todos estos estudios?",
|
||||||
|
"warning_create_new_study_from_series_html": "Ya existe un paciente con el mismo ID de paciente con etiquetas diferentes. ¿Desea utilizar las etiquetas PatientName, PatientSex and PatientBirthDate existentes?"
|
||||||
|
},
|
||||||
|
"my_jobs": "Mis trabajos",
|
||||||
|
"no_modalities": "Ninguno",
|
||||||
|
"no_result_found": "¡No se encontraron resultados!",
|
||||||
|
"not_showing_all_results": "No se muestran todos los resultados. Debes afinar tus criterios de búsqueda. ",
|
||||||
|
"open": "Abrir",
|
||||||
|
"page_not_found": "¡Página no encontrada!",
|
||||||
|
"plugin": "Complemento",
|
||||||
|
"preview": "Vista previa",
|
||||||
|
"profile": "Perfil",
|
||||||
|
"retrieve_and_view": {
|
||||||
|
"finding_locally": "Verificando si el estudio está disponible localmente.",
|
||||||
|
"finding_remotely": "Buscando el estudio en nodos DICOM remotos.",
|
||||||
|
"not_found": "El estudio no se encontró ni localmente ni en nodos DICOM remotos.",
|
||||||
|
"retrieved_html": "Se recuperaron <strong>{count}</strong> elementos.",
|
||||||
|
"retrieving": "Recuperando estudio."
|
||||||
|
},
|
||||||
|
"searching": "Buscando...",
|
||||||
|
"select_files": "Seleccionar Archivos",
|
||||||
|
"select_folder": "Seleccionar Carpeta",
|
||||||
|
"send_to": {
|
||||||
|
"button_title": "Enviar",
|
||||||
|
"dicom": "Enviar a nodo DICOM",
|
||||||
|
"dicom_web": "Enviar a DICOM-Web",
|
||||||
|
"orthanc_peer": "Enviar a un par",
|
||||||
|
"transfers": "Enviar a un par (Transferencias avanzadas)"
|
||||||
|
},
|
||||||
|
"series": "Serie",
|
||||||
|
"series_count_header": "# serie",
|
||||||
|
"series_plural": "Series",
|
||||||
|
"settings": {
|
||||||
|
"dicom_AET": "DICOM AET",
|
||||||
|
"dicom_port": "Puerto DICOM",
|
||||||
|
"ingest_transcoding": "Transcodificación adaptable",
|
||||||
|
"installed_plugins": "Complementos instalados",
|
||||||
|
"orthanc_name": "Nombre original",
|
||||||
|
"orthanc_system_info": "Información del sistema Orthanc",
|
||||||
|
"orthanc_version": "Versión original",
|
||||||
|
"overwrite_instances": "Sobrescribir elementos",
|
||||||
|
"plugins_not_enabled": "Los complementos que están cargados pero no habilitados o configurados correctamente están ",
|
||||||
|
"statistics": "Estadísticas",
|
||||||
|
"storage_compression": "Compresión de almacenamiento",
|
||||||
|
"storage_size": "Tamaño de almacenamiento",
|
||||||
|
"striked_through": "tachado",
|
||||||
|
"title": "Configuraciones",
|
||||||
|
"verbosity_level": "Nivel de Verbosidad"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"anonymize": "¿Anonimizar?",
|
||||||
|
"button_title": "Compartir",
|
||||||
|
"copy_and_close": "Copiar y cerrar",
|
||||||
|
"days": "días",
|
||||||
|
"expires_in": "Expira en",
|
||||||
|
"link": "Enlace",
|
||||||
|
"modal_title": "Compartir estudio",
|
||||||
|
"never": "nunca"
|
||||||
|
},
|
||||||
|
"show_errors": "Mostrar errores",
|
||||||
|
"studies": "Estudios",
|
||||||
|
"study": "Estudio",
|
||||||
|
"this_patient_has_no_other_studies": "Este paciente no tiene otros estudios.",
|
||||||
|
"this_patient_has_other_studies": "Este paciente tiene {count} estudios en total.",
|
||||||
|
"this_patient_has_other_studies_show": "Mostrarlos!",
|
||||||
|
"token": {
|
||||||
|
"error_token_expired_html": "Tu token ha caducado.",
|
||||||
|
"error_token_invalid_html": "Tu token es inválido.<br/>Comprueba que lo has pegado por completo o contacta a la persona que te lo ha facilitado.",
|
||||||
|
"error_token_unknown_html": "Tu token es inválido.<br/>Comprueba que lo has pegado por completo o contacta a la persona que te lo ha facilitado.",
|
||||||
|
"token_being_checked_html": "Tu token está siendo verificado."
|
||||||
|
},
|
||||||
|
"trace": "Rastreo",
|
||||||
|
"upload": "Cargar",
|
||||||
|
"uploaded_studies": "Estudios cargados",
|
||||||
|
"verbose": "Detallado",
|
||||||
|
"view_in_meddream": "Ver en MedDream",
|
||||||
|
"view_in_ohif": "Ver en OHIF",
|
||||||
|
"view_in_osimis": "Ver en OsimisViewer",
|
||||||
|
"view_in_stone": "Ver en StoneViewer",
|
||||||
|
"view_in_volview": "Ver en VolView"
|
||||||
|
}
|
||||||
270
WebApplication/src/locales/fr.json
Normal file
270
WebApplication/src/locales/fr.json
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
{
|
||||||
|
"add_series": {
|
||||||
|
"button_title": "Ajouter une série",
|
||||||
|
"drop_file": "Déposez ici un fichier PDF, image ou STL",
|
||||||
|
"error_add_series_unexpected_error_html": "Erreur inattendue lors de l'ajout de la sérier', vérifiez les logs d'Orthanc",
|
||||||
|
"modal_title": "Ajouter une nouvelle série à l'examen",
|
||||||
|
"select_file": "Choisir un fichier",
|
||||||
|
"unrecognized_file_format": "Format de fichier non reconnu. Seuls les format PDF, images (JPG et PNG) et STL sont supportés.",
|
||||||
|
"uploaded_file": "Le fichier {name} ({size}) est de type {type}."
|
||||||
|
},
|
||||||
|
"all_modalities": "Toutes",
|
||||||
|
"anonymize": "Anonymiser",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"change_password": "Changer le MDP",
|
||||||
|
"close": "Fermer",
|
||||||
|
"copy_orthanc_id": "Copier l'identifiant Orthanc",
|
||||||
|
"date_picker": {
|
||||||
|
"last_12_months": "Les 12 derniers mois",
|
||||||
|
"last_month": "Le mois dernier",
|
||||||
|
"last_week": "La semaine dernière",
|
||||||
|
"this_month": "Ce mois-ci",
|
||||||
|
"this_week": "Cette semaine",
|
||||||
|
"today": "Aujourd'hui",
|
||||||
|
"yesterday": "Hier"
|
||||||
|
},
|
||||||
|
"default": "Par défaut",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"delete_instance_body": "Êtes vous sûr de vouloir supprimer cette instance ?<br/>Cette opération est définitive.",
|
||||||
|
"delete_instance_title": "Supprimer l'instance ?",
|
||||||
|
"delete_series_body": "Êtes vous sûr de vouloir supprimer cette série ?<br/>Cette opération est définitive.",
|
||||||
|
"delete_series_title": "Supprimer la série ?",
|
||||||
|
"delete_studies_body": "Êtes vous sûr de vouloir supprimer ces examens ?<br/>Cette opération est définitive.",
|
||||||
|
"delete_studies_title": "Supprimer les examens ?",
|
||||||
|
"delete_study_body": "Êtes vous sûr de vouloir supprimer cet examen ?<br/>Cette opération est définitive.",
|
||||||
|
"delete_study_title": "Supprimer l'examen ?",
|
||||||
|
"description": "Description",
|
||||||
|
"dicom_modalities": "Modalités DICOM",
|
||||||
|
"dicom_tags": {
|
||||||
|
"AccessionNumber": "Numéro d'accès",
|
||||||
|
"BodyPartExamined": "Partie du corps examinée",
|
||||||
|
"InstanceNumber": "Numéro d'instance",
|
||||||
|
"InstitutionName": "Nom de l'établissement",
|
||||||
|
"ModalitiesInStudy": "Modalités",
|
||||||
|
"Modality": "Modalité",
|
||||||
|
"NumberOfFrames": "Nombre d'images",
|
||||||
|
"NumberOfStudyRelatedSeries": "Nombre de séries",
|
||||||
|
"OtherPatientIDs": "Autres ID Patient",
|
||||||
|
"PatientBirthDate": "Date de naissance",
|
||||||
|
"PatientID": "ID Patient",
|
||||||
|
"PatientName": "Nom",
|
||||||
|
"PatientSex": "Sexe",
|
||||||
|
"ProtocolName": "Nom du protocole",
|
||||||
|
"ReferringPhysicianName": "Nom du médecin référent",
|
||||||
|
"RequestingPhysician": "Médecin demandeur",
|
||||||
|
"SOPInstanceUID": "SOP Instance UID",
|
||||||
|
"SeriesDate": "Date de la série",
|
||||||
|
"SeriesDescription": "Description",
|
||||||
|
"SeriesInstanceUID": "Series Instance UID",
|
||||||
|
"SeriesNumber": "Numéro de série",
|
||||||
|
"SeriesTime": "Heure de la série",
|
||||||
|
"StudyDate": "Date de l'examen",
|
||||||
|
"StudyDescription": "Description",
|
||||||
|
"StudyID": "ID de l'examen",
|
||||||
|
"StudyInstanceUID": "Study Instance UID",
|
||||||
|
"StudyTime": "Heure de l'examen",
|
||||||
|
"seriesCount": "Nombre de séries"
|
||||||
|
},
|
||||||
|
"dicom_web_servers": "Serveurs Web DICOM",
|
||||||
|
"displaying_latest_studies": "Liste des examens les plus récents",
|
||||||
|
"download_dicom_file": "Télécharger le fichier DICOM",
|
||||||
|
"download_dicomdir": "Télécharger DICOMDIR",
|
||||||
|
"download_zip": "Télécharger ZIP",
|
||||||
|
"drop_files": "Déposez les fichiers ici ou",
|
||||||
|
"enter_search": "Entrez un critère de recherche pour afficher les résultats !",
|
||||||
|
"error": "Erreur",
|
||||||
|
"file": "Fichier",
|
||||||
|
"files": "fichiers",
|
||||||
|
"frames": "Frames",
|
||||||
|
"instance": "Élément",
|
||||||
|
"instances": "Instances",
|
||||||
|
"instances_count_header": "# instances",
|
||||||
|
"instances_number": "Nombre d'instances",
|
||||||
|
"job_success": "Succès",
|
||||||
|
"job_failure": "Échec",
|
||||||
|
"labels": {
|
||||||
|
"add_button": "Ajouter",
|
||||||
|
"add_labels_message_html": "<strong>Ajouter</strong> ces étiquettes aux examens sélectionnés",
|
||||||
|
"add_labels_placeholder": "Ajouter des étiquettes, appuyer sur Enter pour en créer de nouvelles",
|
||||||
|
"add_labels_placeholder_no_create": "Ajouter des étiquettes",
|
||||||
|
"added_labels_message_part_1_html": "Les étiquettes ",
|
||||||
|
"added_labels_message_part_2_html": " ont été ajoutées aux <strong>{count}</strong> examens",
|
||||||
|
"clear_all_button": "Tout effacer",
|
||||||
|
"clear_all_labels_message_html": "<strong>Effacer toutes</strong> les étiquettes des examens sélectionnés",
|
||||||
|
"cleared_labels_message_part_1_html": "Les étiquettes ",
|
||||||
|
"cleared_labels_message_part_2_html": " ont été supprimées de <strong>{count}</strong> examens",
|
||||||
|
"edit_labels_button": "Modifier les étiquettes",
|
||||||
|
"modal_title": "Modifier les étiquettes dans les examens sélectionnés",
|
||||||
|
"remove_button": "Supprimer",
|
||||||
|
"remove_labels_message_html": "<strong>Supprimer</strong> ces étiquettes des examens sélectionnés",
|
||||||
|
"remove_labels_placeholder": "Étiquettes à supprimer",
|
||||||
|
"removed_labels_message_part_1_html": "Les étiquettes ",
|
||||||
|
"removed_labels_message_part_2_html": " ont été supprimées de <strong>{count}</strong> examens",
|
||||||
|
"study_details_title": "Étiquettes",
|
||||||
|
"valid_alphabet_warning": "Seuls les caractères suivant sont autorisé : lettres sans accents, chiffres, - et _"
|
||||||
|
},
|
||||||
|
"legacy_ui": "Ancienne interface",
|
||||||
|
"loading_latest_studies": "Chargement des examens les plus récents",
|
||||||
|
"local_studies": "Tous les examens",
|
||||||
|
"logout": "Se déconnecter",
|
||||||
|
"mammography_plugin": "Start mammogram AI",
|
||||||
|
"modalities_in_study": "Modalités dans l'examen",
|
||||||
|
"modify": {
|
||||||
|
"anonymize_button_title": "Anonymiser",
|
||||||
|
"anonymize_modal_title": "Anonymiser",
|
||||||
|
"autogenerated_dicom_uid": "DICOM UID autogénéré",
|
||||||
|
"back_button_title": "Précédent",
|
||||||
|
"error_attach_series_to_existing_study_target_does_not_exist_html": "Impossible de modifier la série, aucun examen trouvé avec ce StudyInstanceUID.",
|
||||||
|
"error_attach_series_to_existing_study_target_unchanged_html": "Impossible de modifier la série, l'examen de destination est le même que l'examen de la série actuelle.",
|
||||||
|
"error_attach_study_to_existing_patient_target_does_not_exist_html": "Erreur lors de la modification du patient, aucun patient trouvé avec cet identifiant. Utilisez plutôt la fonction <strong>Modifier les tags de l'examen</strong>.",
|
||||||
|
"error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Erreur lors de la modification des tags de l'examen : ce patient possède d'autres examens. Utilisez plutôt la fonction <strong>Modifier les tags du Patient</strong>.",
|
||||||
|
"error_modify_any_study_tags_patient_exists_html": "Erreur lors de la modification des tags de l'examen : un autre patient existe déjà avec le même identifiant. Utilisez plutôt la fonction <strong>Changer de patient</strong>.",
|
||||||
|
"error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Erreur lors de la modification des tags du patient : un autre patient existe déjà avec le même identifiant. Utilisez plutôt la fonction <strong>Modifier les tags du Patient</strong>.",
|
||||||
|
"error_modify_unexpected_error_html": "Erreur inattendue lors de la modification, vérifiez les logs d'Orthanc",
|
||||||
|
"insert_tag": "Insérer un tag DICOM",
|
||||||
|
"job_in_progress": "La modification est en cours de traitement",
|
||||||
|
"modify_button_title": "Modifier",
|
||||||
|
"modify_modal_title": "Modifier",
|
||||||
|
"modify_series_mode_duplicate_html": "Créer une copie modifiée de la série originale. <small>(en générant de nouveaux UID DICOM et en conservant la série originale)</small>",
|
||||||
|
"modify_series_mode_keep_uids_html": "Modifier la série originale. <small>(en conservant les UID DICOM d'origine)</small>",
|
||||||
|
"modify_series_mode_new_uids_html": "Modifier la série originale. <small>(en générant de nouveaux UID DICOM)</small>",
|
||||||
|
"modify_study_mode_duplicate_html": "Créer une copie modifiée de l'examen original. <small>(en générant de nouveaux UID DICOM et en conservant l'examen original)</small>",
|
||||||
|
"modify_study_mode_keep_uids_html": "Modifier l'examen original. <small>(en conservant les UID DICOM d'origine)</small>",
|
||||||
|
"modify_study_mode_new_uids_html": "Modifier l'examen original. <small>(en générant de nouveaux UID DICOM)</small>",
|
||||||
|
"remove_tag_tooltip": "Supprimer le tag",
|
||||||
|
"remove_tag_undo_tooltip": "Annuler la suppression du tag",
|
||||||
|
"series_step_0_attach_series_to_existing_study_button_title_html": "Déplacez dans un examen existant",
|
||||||
|
"series_step_0_attach_series_to_existing_study_html": "Déplacez cette série vers un examen existant.",
|
||||||
|
"series_step_0_create_new_study_button_title_html": "Créer un nouvel examen",
|
||||||
|
"series_step_0_create_new_study_html": "Déplacez cette série dans un nouvel examen.",
|
||||||
|
"series_step_0_modify_series_button_title_html": "Modifier les tags de la série",
|
||||||
|
"series_step_0_modify_series_html": "Modifier les tags relatifs à la série uniquement dans cette série ?",
|
||||||
|
"show_modified_resources": "Afficher les ressources modifiées",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_button_title_html": "Changer de patient",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_html": "Attachez cet examen à un autre patient existant.",
|
||||||
|
"study_step_0_modify_study_button_title_html": "Modifier les tags de l'examen",
|
||||||
|
"study_step_0_modify_study_html": "Modifier les tags relatif au patient et à l'examen uniquement dans cet examen ?",
|
||||||
|
"study_step_0_patient_has_other_studies_button_title_html": "Modifier les tags du Patient",
|
||||||
|
"study_step_0_patient_has_other_studies_html": "Ce patient a <strong>{count}</strong> examen au total.<br/>Voulez-vous modifier le patient dans tous ces examens ?",
|
||||||
|
"warning_create_new_study_from_series_html": "Un patient avec le même identifiant existe déjà avec des tags différents. Voulez-vous utiliser les nom, sexe et date de naissance du patient existant ?"
|
||||||
|
},
|
||||||
|
"my_jobs": "Mes jobs",
|
||||||
|
"no_modalities": "Aucune",
|
||||||
|
"no_result_found": "Aucun résultat trouvé !",
|
||||||
|
"not_showing_all_results": "Tous les résultats ne sont pas affichés. Veuillez affiner vos critères de recherche",
|
||||||
|
"open": "Ouvrir",
|
||||||
|
"page_not_found": "Page introuvable !",
|
||||||
|
"patients": "Patients",
|
||||||
|
"plugin": "Plugin",
|
||||||
|
"plugins": {
|
||||||
|
"delayed_deletion": {
|
||||||
|
"pending_files_count": "Delayed Deletion plugin: Fichiers à effacer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preview": "Aperçu",
|
||||||
|
"profile": "Profil",
|
||||||
|
"retrieve": "Rapatrier",
|
||||||
|
"retrieve_and_view": {
|
||||||
|
"finding_locally": "Recherche de l'examen en local.",
|
||||||
|
"finding_remotely": "Recherche de l'examen dans les modalités DICOM.",
|
||||||
|
"not_found": "Cet examen n'a pu être trouvé ni en local ni dans les modalités DICOM.",
|
||||||
|
"retrieved_html": "<strong>{count}</strong> instances récupérées.",
|
||||||
|
"retrieving": "L'examen est en cours de récupération."
|
||||||
|
},
|
||||||
|
"remote_dicom_browsing": "Vous explorez actuellement le contenu du noeud <strong>DICOM</strong> <strong>{source}</strong>",
|
||||||
|
"remote_dicom_web_browsing": "Vous explorez actuellement le contenu du serveur <strong>DICOMWeb</strong> <strong>{source}</strong>",
|
||||||
|
"save": "Enregistrer",
|
||||||
|
"searching": "Recherche...",
|
||||||
|
"select_files": "Choisir des fichiers",
|
||||||
|
"select_folder": "Choisir un dossier",
|
||||||
|
"send_to": {
|
||||||
|
"button_title": "Envoyer",
|
||||||
|
"dicom": "Envoyer vers un noeud DICOM",
|
||||||
|
"dicom_web": "Envoyer vers un serveur DICOMWeb",
|
||||||
|
"orthanc_peer": "Envoyer à un peer Orthanc",
|
||||||
|
"transfers": "Envoyer à un peer Orthanc (advanced-transfers)"
|
||||||
|
},
|
||||||
|
"series": "Série",
|
||||||
|
"series_count_header": "# séries",
|
||||||
|
"series_and_instances_count_header": "# Ser/Inst",
|
||||||
|
"series_plural": "Séries",
|
||||||
|
"settings": {
|
||||||
|
"allow_all_currents_and_futures_labels": "Toutes les étiquettes actuelles et futures",
|
||||||
|
"available_labels_global_instructions_html": "Les permissions étant activées, cette page vous permet de modifier la liste des étiquettes disponibles. Les autres utilisateurs ne pourront pas créer de nouvelles étiquettes.<br/>N'oubliez pas d'enregistrer vos modifications.",
|
||||||
|
"available_labels_title": "Étiquettes disponibles",
|
||||||
|
"create_new_label": "Créer une étiquette",
|
||||||
|
"dicom_AET": "DICOM AET",
|
||||||
|
"dicom_port": "Port DICOM",
|
||||||
|
"ingest_transcoding": "Transcodage à la réception",
|
||||||
|
"installed_plugins": "Plugins installés",
|
||||||
|
"labels_clear_selection": "Vider la sélection",
|
||||||
|
"labels_description_html": "Les étiquettes sont créées dans la pages Paramètres/Étiquettes.",
|
||||||
|
"labels_select_all_currents": "Sélectionner toutes les étiquettes actuellement disponibles.",
|
||||||
|
"labels_limit_available_labels": "Restreindre les étiquettes à une liste",
|
||||||
|
"labels_list_empty": "La liste des étiquettes est actuellement vide",
|
||||||
|
"labels_based_permissions_title": "Permissions basées sur les étiquettes",
|
||||||
|
"labels_title": "Étiquettes",
|
||||||
|
"global_permissions_title": "Permissions globales",
|
||||||
|
"orthanc_name": "Nom de l'Orthanc",
|
||||||
|
"orthanc_system_info": "Informations système Orthanc",
|
||||||
|
"orthanc_version": "Version d'Orthanc",
|
||||||
|
"overwrite_instances": "Écraser les instances",
|
||||||
|
"permissions": "Permissions",
|
||||||
|
"permissions_clear_selection": "Vider la sélection",
|
||||||
|
"permissions_global_instructions_html": "Éditer les permissions ci-dessous et cliquez sur <strong>Enregistrer</strong> pour appliquer les modifications. <br>Pour chaque rôle, sélectionnez les étiquettes qui seront accessibles et les actions autorisées sur ces examens.",
|
||||||
|
"permissions_instructions": "La liste de permission ne peut être modifiée; elle est définie dans le plugin authorization.",
|
||||||
|
"plugins_not_enabled": "Les plugins qui sont chargés mais non-activés ou configurés correctement sont ",
|
||||||
|
"read_only": "Système en lecture seule",
|
||||||
|
"roles_title": "Rôles",
|
||||||
|
"roles_description_html": "Les rôles sont créés et modifiés dans Keycloak",
|
||||||
|
"select_all_label_permissions": "Sélectionner toutes les permissions basées sur les étiquettes",
|
||||||
|
"select_all_global_permissions": "Sélectionner toutes les permissions globales",
|
||||||
|
"system_info": "Système",
|
||||||
|
"statistics": "Statistiques",
|
||||||
|
"storage_compression": "Compression du stockage",
|
||||||
|
"storage_size": "Espace utilisé",
|
||||||
|
"striked_through": "barrés",
|
||||||
|
"title": "Paramètres",
|
||||||
|
"verbosity_level": "Niveau de verbosité des logs"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"anonymize": "Anonymiser ?",
|
||||||
|
"button_title": "Partager",
|
||||||
|
"copy_and_close": "Copier et fermer",
|
||||||
|
"days": "jours",
|
||||||
|
"expires_in": "Expire dans",
|
||||||
|
"link": "Lien",
|
||||||
|
"modal_title": "Partager l'examen",
|
||||||
|
"modal_title_multiple_studies": "Partager {count} examens",
|
||||||
|
"never": "jamais"
|
||||||
|
},
|
||||||
|
"show_errors": "Afficher les erreurs",
|
||||||
|
"studies": "Examens",
|
||||||
|
"study": "Examen",
|
||||||
|
"this_patient_has_no_other_studies": "Ce patient n'a pas d'autres examens.",
|
||||||
|
"this_patient_has_other_studies": "Ce patient a {count} examens au total.",
|
||||||
|
"this_patient_has_other_studies_show": "Les afficher !",
|
||||||
|
"this_remote_patient_has_local_studies": "Ce patient a déjà {count} examens stockés en local.",
|
||||||
|
"this_study_is_already_stored_locally": "Cet examen est déjà stocké en local.",
|
||||||
|
"this_study_is_already_stored_locally_show": "L'afficher !",
|
||||||
|
"token": {
|
||||||
|
"error_token_expired_html": "Votre lien a expiré.",
|
||||||
|
"error_token_invalid_html": "Votre lien n'est pas valide.<br/>Vérifiez que vous l'avez collé complètement ou contactez la personne qui vous l'a fourni.",
|
||||||
|
"error_token_unknown_html": "Votre lien n'est pas valide.<br/>Vérifiez que vous l'avez collé complètement ou contactez la personne qui vous l'a fourni.",
|
||||||
|
"token_being_checked_html": "Votre lien est en cours de vérification."
|
||||||
|
},
|
||||||
|
"trace": "Trace",
|
||||||
|
"upload": "Importer",
|
||||||
|
"uploaded_studies": "Examens importés",
|
||||||
|
"verbose": "Verbose",
|
||||||
|
"view_in_meddream": "Visualiser avec MedDream",
|
||||||
|
"view_in_ohif": "Visualiser avec OHIF",
|
||||||
|
"view_in_ohif_microscopy": "Visualiser avec OHIF en mode Microscopie",
|
||||||
|
"view_in_ohif_seg": "Visualiser avec OHIF en mode Segmentation",
|
||||||
|
"view_in_ohif_tmtv": "Visualiser avec OHIF en mode Total Metabolic Tumor Volume",
|
||||||
|
"view_in_ohif_vr": "Visualiser avec OHIF en mode Volume Rendering",
|
||||||
|
"view_in_osimis": "Visualiser avec le OsimisViewer",
|
||||||
|
"view_in_stone": "Visualiser avec le StoneViewer",
|
||||||
|
"view_in_volview": "Visualiser avec VolView",
|
||||||
|
"view_in_wsi_viewer": "Visualiser avec le Whole Slide Imaging Viewer"
|
||||||
|
}
|
||||||
48
WebApplication/src/locales/i18n.js
Normal file
48
WebApplication/src/locales/i18n.js
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { createI18n } from "vue-i18n";
|
||||||
|
import en from "./en.json";
|
||||||
|
import de from "./de.json";
|
||||||
|
import es from "./es.json";
|
||||||
|
import fr from "./fr.json";
|
||||||
|
import it from "./it.json";
|
||||||
|
import ka from "./ka.json";
|
||||||
|
import ru from "./ru.json";
|
||||||
|
import si from "./si.json";
|
||||||
|
import uk from "./uk.json";
|
||||||
|
import zh from "./zh.json";
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
warnHtmlInMessage: 'off',
|
||||||
|
locale: 'en', // when the list of availableLanguages is loaded, this value is updated in LanguagePicker.isConfigurationLoaded
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
messages: {
|
||||||
|
en,
|
||||||
|
de,
|
||||||
|
es,
|
||||||
|
fr,
|
||||||
|
it,
|
||||||
|
ka,
|
||||||
|
ru,
|
||||||
|
si,
|
||||||
|
uk,
|
||||||
|
zh
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
document._mustTranslateDicomTags = false;
|
||||||
|
|
||||||
|
function translateDicomTag($t, $te, tagName) {
|
||||||
|
if (document._mustTranslateDicomTags) {
|
||||||
|
if ($te('dicom_tags.' + tagName)) {
|
||||||
|
return $t('dicom_tags.' + tagName)
|
||||||
|
} else {
|
||||||
|
return tagName;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return tagName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
i18n as default,
|
||||||
|
translateDicomTag
|
||||||
|
};
|
||||||
190
WebApplication/src/locales/it.json
Normal file
190
WebApplication/src/locales/it.json
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
{
|
||||||
|
"all_modalities": "Tutte le modalità",
|
||||||
|
"anonymize": "Anonimizzare",
|
||||||
|
"cancel": "Annulla",
|
||||||
|
"change_password": "Cambia password",
|
||||||
|
"close": "Chiudi",
|
||||||
|
"copy_orthanc_id": "Copia Orthanc ID",
|
||||||
|
"date_picker": {
|
||||||
|
"last_12_months": "12 Mesi precedenti ",
|
||||||
|
"last_month": "Mese Precedente",
|
||||||
|
"last_week": "Scorsa settimana",
|
||||||
|
"this_month": "Questo mese",
|
||||||
|
"this_week": "Questa settimana",
|
||||||
|
"today": "Oggi",
|
||||||
|
"yesterday": "Ieri"
|
||||||
|
},
|
||||||
|
"default": "Predefinito",
|
||||||
|
"delete": "Cancella",
|
||||||
|
"delete_instance_body": "Sei sicuro di voler cancellare questa istanza?<br/>Questa azione non potrà essere annullata!",
|
||||||
|
"delete_instance_title": "Cancellare istanza?",
|
||||||
|
"delete_series_body": "Sei sicuro di voler cancellare la serie?<br/>Questa azione non potrà essere annullata!",
|
||||||
|
"delete_series_title": "Cancellare la serie?",
|
||||||
|
"delete_studies_body": "Sei sicuro di voler cancellare questi studi?<br/>Questa azione non potrà essere annullata!",
|
||||||
|
"delete_studies_title": "Cancellare gli studi?",
|
||||||
|
"delete_study_body": "Sei sicuro di voler cancellare lo studio?<br/>Questa azione non potrà essere annullata!",
|
||||||
|
"delete_study_title": "Cancellare lo Studio?",
|
||||||
|
"description": "Descrizione",
|
||||||
|
"dicom_modalities": "DICOM Modalità",
|
||||||
|
"dicom_tags": {
|
||||||
|
"AccessionNumber": "Numero Accesso",
|
||||||
|
"BodyPartExamined": "Parte del corpo esaminata",
|
||||||
|
"InstanceNumber": "Numero Istanza",
|
||||||
|
"InstitutionName": "Nome Istituto",
|
||||||
|
"ModalitiesInStudy": "Modalità nello Studio",
|
||||||
|
"Modality": "Modalità",
|
||||||
|
"NumberOfFrames": "Numero immagini",
|
||||||
|
"NumberOfStudyRelatedSeries": "Numero serie",
|
||||||
|
"PatientBirthDate": "Data di Nascita Paziente",
|
||||||
|
"PatientID": "ID Paziente",
|
||||||
|
"PatientName": "Nome Paziente",
|
||||||
|
"PatientSex": "Sesso Paziente",
|
||||||
|
"ProtocolName": "Nome Protocollo",
|
||||||
|
"ReferringPhysicianName": "Nome Medico Referimento",
|
||||||
|
"RequestingPhysician": "Medico Richiedente",
|
||||||
|
"SeriesDate": "Data Serie",
|
||||||
|
"SeriesDescription": "Descrizione Serie",
|
||||||
|
"SeriesInstanceUID": "Instanza UID Serie",
|
||||||
|
"SeriesNumber": "Numero Serie",
|
||||||
|
"SeriesTime": "Ora Serie",
|
||||||
|
"StudyDate": "Data Studio",
|
||||||
|
"StudyDescription": "Descrizione Studio",
|
||||||
|
"StudyID": "ID Studio",
|
||||||
|
"StudyInstanceUID": "Instanza UID Studio",
|
||||||
|
"StudyTime": "Ora Studio",
|
||||||
|
"seriesCount": "Numero di serie"
|
||||||
|
},
|
||||||
|
"dicom_web_servers": "DICOM-Web Servers",
|
||||||
|
"displaying_latest_studies": "Visualizza studio più recente",
|
||||||
|
"download_dicom_file": "Scarica DICOM file",
|
||||||
|
"download_dicomdir": "Scarica DICOMDIR",
|
||||||
|
"download_zip": "Scarica ZIP",
|
||||||
|
"drop_files": "Trascina files qui oppure",
|
||||||
|
"enter_search": "Inserisci un criterio di ricerca per mostrare i risultati!",
|
||||||
|
"error": "Errore",
|
||||||
|
"file": "File",
|
||||||
|
"files": "files",
|
||||||
|
"frames": "immagini",
|
||||||
|
"instance": "Istanza",
|
||||||
|
"instances": "Istanze",
|
||||||
|
"instances_number": "Numero di istanze",
|
||||||
|
"legacy_ui": "Storica UI",
|
||||||
|
"loading_latest_studies": "Caricamento studio più recente",
|
||||||
|
"local_studies": "Studi Locali",
|
||||||
|
"logout": "Disconnettersi",
|
||||||
|
"mammography_plugin": "Start mammogram AI",
|
||||||
|
"modalities_in_study": "Modalità nello Studio",
|
||||||
|
"modify": {
|
||||||
|
"anonymize_button_title": "Anonimizzare",
|
||||||
|
"anonymize_modal_title": "Anonimizzare",
|
||||||
|
"autogenerated_dicom_uid": "Generato automaticamente",
|
||||||
|
"back_button_title": "Indietro",
|
||||||
|
"error_attach_series_to_existing_study_target_does_not_exist_html": "Errore nel cambiare la serie, nessun studio trovato con questa Instanza UID Studio.",
|
||||||
|
"error_attach_series_to_existing_study_target_unchanged_html": "Errore nel cambiare la serie, lo studio in oggetto è identico alla serie studio corrente.",
|
||||||
|
"error_attach_study_to_existing_patient_target_does_not_exist_html": "Errore nel cambiare il paziente, nessun paziente trovato con questo ID Paziente. Pobabilmente dovresti usare la funzione <strong>Modifica per le Etichette dello Studio</strong>.",
|
||||||
|
"error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Errore nella modifica delle etichette Studio: ci sono altri studi per questo paziente. Dovresti usare la funzione <strong>Modifica etichette Paziente</strong>.",
|
||||||
|
"error_modify_any_study_tags_patient_exists_html": "Errore nella modifica delle etichette studio: esiste già un altro paziente con lo stesso ID Paziente. Dovresti usare la funzione <strong>Cambia paziente</strong>.",
|
||||||
|
"error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Errore nella modifica delle etichette Paziente: esiste un altro paziente con lo stesso ID Paziente. Dovresti usare la funzione <strong>Modifica etichette Paziente</strong>.",
|
||||||
|
"error_modify_unexpected_error_html": "Errore inaspettato durante la modifica, verifica i log di Orthanc",
|
||||||
|
"insert_tag": "Inserisci un'etichetta DICOM",
|
||||||
|
"job_in_progress": "La modifica è in fase di elaborazione",
|
||||||
|
"modify_button_title": "Modifica",
|
||||||
|
"modify_modal_title": "Modifica",
|
||||||
|
"modify_study_mode_duplicate_html": "Creare una copia modificata dello studio originale. <small>(generazione di un nuovo DICOM UIDs e mantenendo lo studio originale)</small>",
|
||||||
|
"modify_study_mode_keep_uids_html": "Modifica lo studio originale. <small>(mantenendo l'originale DICOM UIDs)</small>",
|
||||||
|
"modify_study_mode_new_uids_html": "Modifica lo studio originale. <small>(generazione di un nuovo DICOM UIDs)</small>",
|
||||||
|
"remove_tag_tooltip": "Rimuovi etichetta",
|
||||||
|
"remove_tag_undo_tooltip": "Annulla rimuovi etichetta",
|
||||||
|
"series_step_0_attach_series_to_existing_study_button_title_html": "Cambia studio",
|
||||||
|
"series_step_0_attach_series_to_existing_study_html": "Sposta questa serie in uno studio esistente.",
|
||||||
|
"series_step_0_create_new_study_button_title_html": "Crea nuovo studio",
|
||||||
|
"series_step_0_create_new_study_html": "Sposta questa serie in un nuovo studio.",
|
||||||
|
"series_step_0_modify_series_button_title_html": "Modifica le etichette di serie",
|
||||||
|
"series_step_0_modify_series_html": "Modifica le etichette di serie solo in questa serie?",
|
||||||
|
"show_modified_resources": "Mostra risorse modificate",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_button_title_html": "Cambia paziente",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_html": "Allegare questo studio a un altro paziente esistente.",
|
||||||
|
"study_step_0_modify_study_button_title_html": "Modifica etichette Studio",
|
||||||
|
"study_step_0_modify_study_html": "Modifica etichette Paziente/Studio solo in questo studio?",
|
||||||
|
"study_step_0_patient_has_other_studies_button_title_html": "Modifica etichette Paziente",
|
||||||
|
"study_step_0_patient_has_other_studies_html": "Questo paziente ha <strong>{count}</strong> studi in totale.<br/>vuoi moficare il paziente in tutti questi studi?",
|
||||||
|
"warning_create_new_study_from_series_html": "Un paziente con lo stesso ID Paziente esiste già con delle etichette differenti. Vuoi usare Nome Paziente, Sesso Paziente e Data di Nascita Paziente esistente?"
|
||||||
|
},
|
||||||
|
"my_jobs": "I miei Lavori",
|
||||||
|
"no_modalities": "Nessuna",
|
||||||
|
"no_result_found": "Nessun risultato trovato!",
|
||||||
|
"not_showing_all_results": "Non vengono visualizzati tutti i risultati. Dovresti affinare i tuoi criteri di ricerca",
|
||||||
|
"open": "Apri",
|
||||||
|
"page_not_found": "Pagina non trovata!",
|
||||||
|
"plugin": "plugin",
|
||||||
|
"preview": "Anteprima",
|
||||||
|
"profile": "Profilo",
|
||||||
|
"retrieve_and_view": {
|
||||||
|
"finding_locally": "Verificare se lo studio è già disponibile localmente.",
|
||||||
|
"finding_remotely": "Ricerca dello studio nei nodi DICOM remoti.",
|
||||||
|
"not_found": "Lo studio non è stato trovato né localmente né in nodi DICOM remoti.",
|
||||||
|
"retrieved_html": "Recuperate <strong>{count}</strong> istanze.",
|
||||||
|
"retrieving": "Recupero dello studio."
|
||||||
|
},
|
||||||
|
"searching": "Cercando...",
|
||||||
|
"select_files": "Seleziona Files",
|
||||||
|
"select_folder": "Seleziona Cartella",
|
||||||
|
"send_to": {
|
||||||
|
"button_title": "Invia",
|
||||||
|
"dicom": "Invia ad un nodo DICOM",
|
||||||
|
"dicom_web": "Invia ad un server DICOMWeb",
|
||||||
|
"orthanc_peer": "Invia ad un Peer Orthanc",
|
||||||
|
"transfers": "Invia ad un Peer Orthanc (trasferimento-avanzato)"
|
||||||
|
},
|
||||||
|
"series": "Serie",
|
||||||
|
"series_count_header": "# serie",
|
||||||
|
"series_plural": "Serie",
|
||||||
|
"settings": {
|
||||||
|
"dicom_AET": "DICOM AET",
|
||||||
|
"dicom_port": "DICOM Porta",
|
||||||
|
"ingest_transcoding": "Transcodifica Ingest",
|
||||||
|
"installed_plugins": "Plugins Installato",
|
||||||
|
"orthanc_name": "Nome Orthanc",
|
||||||
|
"orthanc_system_info": "Informazioni di sistema Orthanc",
|
||||||
|
"orthanc_version": "Versione Orthanc",
|
||||||
|
"overwrite_instances": "Sovrascrivi istanze",
|
||||||
|
"plugins_not_enabled": "Plugins caricato non è abilitato oppure non è configurato correttamente",
|
||||||
|
"statistics": "Statistiche",
|
||||||
|
"storage_compression": "Compressione di archiviazione",
|
||||||
|
"storage_size": "Dimensioni di archiviazione",
|
||||||
|
"striked_through": "Colpito",
|
||||||
|
"title": "Impostazioni",
|
||||||
|
"verbosity_level": "Livello Prolisso"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"anonymize": "Anonimizzare?",
|
||||||
|
"button_title": "Condividi",
|
||||||
|
"copy_and_close": "Copia e Chiudi",
|
||||||
|
"days": "giorni",
|
||||||
|
"expires_in": "Scade tra",
|
||||||
|
"link": "Collegamento",
|
||||||
|
"modal_title": "Condividi studio",
|
||||||
|
"never": "mai"
|
||||||
|
},
|
||||||
|
"show_errors": "mostra errori",
|
||||||
|
"studies": "Studi",
|
||||||
|
"study": "Studio",
|
||||||
|
"this_patient_has_no_other_studies": "Questo paziente non ha altri studi.",
|
||||||
|
"this_patient_has_other_studies": "Questo paziente ha {count} studi in totale.",
|
||||||
|
"this_patient_has_other_studies_show": "Mostra altri!",
|
||||||
|
"token": {
|
||||||
|
"error_token_expired_html": "Il tuo token è scaduto.",
|
||||||
|
"error_token_invalid_html": "Il tuo token non è valido.<br/>Controlla di averlo incollato completamente o contatta la persona che te lo ha fornito.",
|
||||||
|
"error_token_unknown_html": "Il tuo token è sconosciuto.<br/>Controlla di averlo incollato completamente o contatta la persona che te lo ha fornito.",
|
||||||
|
"token_being_checked_html": "Il tuo token è in fase di verifica."
|
||||||
|
},
|
||||||
|
"trace": "Traccia",
|
||||||
|
"upload": "caricare",
|
||||||
|
"uploaded_studies": "Studi caricati",
|
||||||
|
"verbose": "Prolisso",
|
||||||
|
"view_in_meddream": "Visualizza con MedDream",
|
||||||
|
"view_in_ohif": "Visualizza con OHIF",
|
||||||
|
"view_in_osimis": "Visualizza con OsimisViewer",
|
||||||
|
"view_in_stone": "Visualizza con StoneViewer",
|
||||||
|
"view_in_volview": "Visualizza con VolView"
|
||||||
|
}
|
||||||
190
WebApplication/src/locales/ka.json
Normal file
190
WebApplication/src/locales/ka.json
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
{
|
||||||
|
"all_modalities": "ყველა",
|
||||||
|
"anonymize": "ანონიმიზაცია",
|
||||||
|
"cancel": "გაუქმება",
|
||||||
|
"change_password": "პაროლის შეცვლა",
|
||||||
|
"close": "დახურვა",
|
||||||
|
"copy_orthanc_id": "Orthanc ID-ს კოპირება",
|
||||||
|
"date_picker": {
|
||||||
|
"last_12_months": "ბოლო 12 თვე",
|
||||||
|
"last_month": "წინა თვე",
|
||||||
|
"last_week": "წინა კვირა",
|
||||||
|
"this_month": "ეს თვე",
|
||||||
|
"this_week": "ეს კვირა",
|
||||||
|
"today": "დღეს",
|
||||||
|
"yesterday": "გუშინ"
|
||||||
|
},
|
||||||
|
"default": "სტანდარტული",
|
||||||
|
"delete": "წაშლა",
|
||||||
|
"delete_instance_body": "ნამდვილად გსურთ ობიექტის წაშლა?<br/> სურათის დაბრუნება შეუძლებელი იქნება",
|
||||||
|
"delete_instance_title": "ობიექტის წაშლა",
|
||||||
|
"delete_series_body": "ნამდვილად გსურთ სერიის წაშლა?<br/> სერიის დაბრუნება შეუძლებელი იქნება",
|
||||||
|
"delete_series_title": "სერიის წაშლა",
|
||||||
|
"delete_studies_body": "ნამდვილად გსურთ კვლევების წაშლა?<br/> კვლევის დაბრუნება შეუძლებელი იქნება",
|
||||||
|
"delete_studies_title": "კვლევების წაშლა",
|
||||||
|
"delete_study_body": "ნამდვილად გსურთ კვლევის წაშლა?<br/> კვლევის დაბრუნება შეუძლებელი იქნება",
|
||||||
|
"delete_study_title": "კვლევის წაშლა",
|
||||||
|
"description": "აღწერა",
|
||||||
|
"dicom_modalities": "DICOM აპარატები",
|
||||||
|
"dicom_tags": {
|
||||||
|
"AccessionNumber": "რიგითობის ID",
|
||||||
|
"BodyPartExamined": "გამოკვლეული სხეულის ნაწილი",
|
||||||
|
"InstanceNumber": "ობიექტების #",
|
||||||
|
"InstitutionName": "დაწესებულება",
|
||||||
|
"ModalitiesInStudy": "აპარატები",
|
||||||
|
"Modality": "აპარატი",
|
||||||
|
"NumberOfFrames": "კადრები",
|
||||||
|
"NumberOfStudyRelatedSeries": "სერიების რაოდენობა",
|
||||||
|
"PatientBirthDate": "პაციენტის დ.თ.",
|
||||||
|
"PatientID": "პაციენტის ID",
|
||||||
|
"PatientName": "სახელი",
|
||||||
|
"PatientSex": "სქესი",
|
||||||
|
"ProtocolName": "პროტოკოლი",
|
||||||
|
"ReferringPhysicianName": "გამომგზავნი",
|
||||||
|
"RequestingPhysician": "დამკვეთი",
|
||||||
|
"SeriesDate": "სერიის თარიღი",
|
||||||
|
"SeriesDescription": "სერიის აღწერა",
|
||||||
|
"SeriesInstanceUID": "სერიის ობიექტის UID",
|
||||||
|
"SeriesNumber": "სერიის #",
|
||||||
|
"SeriesTime": "სერიის დრო",
|
||||||
|
"StudyDate": "თარიღი",
|
||||||
|
"StudyDescription": "კვლევის აღწერა",
|
||||||
|
"StudyID": "კვლევის ID",
|
||||||
|
"StudyInstanceUID": "კვლევის ობიექტის UID",
|
||||||
|
"StudyTime": "დრო",
|
||||||
|
"seriesCount": "სერიები"
|
||||||
|
},
|
||||||
|
"dicom_web_servers": "DICOM-Web სერვერები",
|
||||||
|
"displaying_latest_studies": "ნაჩვენებია უახლესი კვლევები",
|
||||||
|
"download_dicom_file": "DICOM ფაილის გადმოწერა",
|
||||||
|
"download_dicomdir": "DICOMDIR გადმოწერა",
|
||||||
|
"download_zip": "ZIP გადმოწერა",
|
||||||
|
"drop_files": "დაყარეთ ფაილები აქ ან",
|
||||||
|
"enter_search": "რეზულტატის სანახავად შეიყვანეთ ძიების კრიტერიუმები",
|
||||||
|
"error": "შეცდომა",
|
||||||
|
"file": "ფაილი",
|
||||||
|
"files": "ფაილები",
|
||||||
|
"frames": "კადრები",
|
||||||
|
"instance": "სურათი",
|
||||||
|
"instances": "სურათები",
|
||||||
|
"instances_number": "სურათების რაოდენობა",
|
||||||
|
"legacy_ui": "ძველი ინტერფეისი",
|
||||||
|
"loading_latest_studies": "იტვირთება უახლესი კვლევები",
|
||||||
|
"local_studies": "ადგილობრივი კვლევები",
|
||||||
|
"logout": "გამოსვლა",
|
||||||
|
"mammography_plugin": "Start mammogram AI",
|
||||||
|
"modalities_in_study": "კვლევაში გამოყენებული აპარატები",
|
||||||
|
"modify": {
|
||||||
|
"anonymize_button_title": "ანონიმიზაცია",
|
||||||
|
"anonymize_modal_title": "ანონიმიზაცია",
|
||||||
|
"autogenerated_dicom_uid": "ავტომატურად გენერირებული",
|
||||||
|
"back_button_title": "უკან",
|
||||||
|
"error_attach_series_to_existing_study_target_does_not_exist_html": "დაფიქსირდა შეცდომა სერიის ცვლილების დროს, ამ კვლევის UID-თ კვლევა არ იძებნება.",
|
||||||
|
"error_attach_series_to_existing_study_target_unchanged_html": "დაფიქსირდა შეცდომა სერიის ცვლილებისას, სამიზნე კვლევა იგივეა რაც არსებული სერიის კვლევა.",
|
||||||
|
"error_attach_study_to_existing_patient_target_does_not_exist_html": "დაფიქსირდა შეცდომა პაციენტის ცვლილების დროს, პაციენტი ამ ID-თ არ იძებნება. სავარაუდოდ უნდა გამოიყენოთ <strong>კვლევის თეგების შეცვლის</strong> ფუნქცია.",
|
||||||
|
"error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "დაფიქსირდა შეცდომა კვლევის თეგების მოდიფიკაციის დროს: ამ პაციენტს უკვე აქვს სხვა კვლევები. უნდა გამოიყენოთ <strong>პაციენტის თეგების ცვლილების</strong> ფუნქცია.",
|
||||||
|
"error_modify_any_study_tags_patient_exists_html": "დაფიქსირდა შეცდომა კვლევის თეგების ცვლილების დროს. არსებობს სხვა პაციენტი იგივე პაციენტის ID-თ. სავარაუდოდ უნდა გამოიყენოთ <strong>პაციენტის ცვლილების</strong> ფუნქცია.",
|
||||||
|
"error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "დაფიქსირდა შეცდომა პაციენტის თეგების მოდიფიკაციის დროს: არსებობს სხვა პაციენტი იგივე პაციენტის ID-თ. უნდა გამოიყენოთ <strong>პაციენტის თეგების ცვლილების</strong> ფუნქცია.",
|
||||||
|
"error_modify_unexpected_error_html": "შეცდომა მოდიფიკაციის დროს, შეამოწმეთ Orthanc ლოგები",
|
||||||
|
"insert_tag": "DICOM თეგის დამატება",
|
||||||
|
"job_in_progress": "მოდიფიკაცია მუშავდება",
|
||||||
|
"modify_button_title": "ცვლილება",
|
||||||
|
"modify_modal_title": "ცვლილება",
|
||||||
|
"modify_study_mode_duplicate_html": "შეიქმნას საწყისი კვლევის მოდიფიცირებული ასლი. <small>( დაგენერირდეს ახალი DICOM UID-ები და შეინახოს საწყისი კვლევა)</small>",
|
||||||
|
"modify_study_mode_keep_uids_html": "შეიცვალოს საწყისი კვლევა <small>(დარჩეს საწყისი DICOM UID-ები)</small>",
|
||||||
|
"modify_study_mode_new_uids_html": "შეიცვალოს საწყისი კვლევა <small>(დაგენერირდეს ახალი DICOM UID-ებს)</small>",
|
||||||
|
"remove_tag_tooltip": "თეგის წაშლა",
|
||||||
|
"remove_tag_undo_tooltip": "წაშლილი თეგის დაბრუნება",
|
||||||
|
"series_step_0_attach_series_to_existing_study_button_title_html": "კვლევის ცვლილება",
|
||||||
|
"series_step_0_attach_series_to_existing_study_html": "ამ სერიის არსებულ კვლევაში გადატანა.",
|
||||||
|
"series_step_0_create_new_study_button_title_html": "ახალი კვლევის შექმნა",
|
||||||
|
"series_step_0_create_new_study_html": "ამ სერიის ახალ კვლევაში გადატანა.",
|
||||||
|
"series_step_0_modify_series_button_title_html": "სერიის თეგების ცვლილება",
|
||||||
|
"series_step_0_modify_series_html": "შეიცვალოს სერიის თეგები მხოლოდ ამ სერიისთვის?",
|
||||||
|
"show_modified_resources": "მოდიფიცირებული რესურსების ნახვა",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_button_title_html": "პაციენტის ცვლილება",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_html": "ამ კვლევის სხვა, არსებულ პაციენტზე მიბმა.",
|
||||||
|
"study_step_0_modify_study_button_title_html": "კვლევის თეგების ცვლილება",
|
||||||
|
"study_step_0_modify_study_html": "შეიცვალოს პაციენტის/კვლევის თეგები მხოლოდ ამ კვლევისთვის?",
|
||||||
|
"study_step_0_patient_has_other_studies_button_title_html": "პაციენტის თეგების ცვლილება",
|
||||||
|
"study_step_0_patient_has_other_studies_html": "ამ პაციენტს ჯამში აქვს<strong>{count}</strong> კვლევა.<br/> გსურთ პაციენტის ცვლილება ყველა კვლევაში?",
|
||||||
|
"warning_create_new_study_from_series_html": "უკვე არსებობს პაციენტი იგივე ID-თ და განსხვავებული თეგებით. გსურთ გამოიყენოთ არსებული სახელი, სქესი და დაბადების თარიღი?"
|
||||||
|
},
|
||||||
|
"my_jobs": "ჩემი დავალებები",
|
||||||
|
"no_modalities": "არცერთი",
|
||||||
|
"no_result_found": "რეზულტატები ვერ მოიძებნა",
|
||||||
|
"not_showing_all_results": "ყველა კვლევა არ არის ნაჩვენები. დააზუსტეთ ძიების კრიტერიუმები.",
|
||||||
|
"open": "გახსნა",
|
||||||
|
"page_not_found": "გვერდი ვერ მოიძებნა",
|
||||||
|
"plugin": "ფლაგინი",
|
||||||
|
"preview": "ნახვა",
|
||||||
|
"profile": "პროფილი",
|
||||||
|
"retrieve_and_view": {
|
||||||
|
"finding_locally": "მოწმდება თუ კვლევა უკვე ლოკალურად არის ხელმისაწვდომი.",
|
||||||
|
"finding_remotely": "იძებნება კვლევა დისტანციურ DICOM აპარატებში.",
|
||||||
|
"not_found": "კვლევა არ იძებნება არც ლოკალურად და არც დისტანციურ DICOM აპარატებში.",
|
||||||
|
"retrieved_html": "გამოთხოვილია <strong>{count}</strong> ობიექტი.",
|
||||||
|
"retrieving": "კვლეევის გამოთხოვა."
|
||||||
|
},
|
||||||
|
"searching": "ძიება...",
|
||||||
|
"select_files": "აირჩიეთ ფაილები",
|
||||||
|
"select_folder": "აირჩიეთ ფოლდერი",
|
||||||
|
"send_to": {
|
||||||
|
"button_title": "გაგზავნა",
|
||||||
|
"dicom": "DICOM აპარატში გაგზავნა",
|
||||||
|
"dicom_web": "DICOMWeb სერვერზე გაგზავნა",
|
||||||
|
"orthanc_peer": "გაგზავნა სხვა Orthanc სისტემაში",
|
||||||
|
"transfers": "სხვა Orthanc სისტემაში გაგზავნა (ოპტიმიზირებული)"
|
||||||
|
},
|
||||||
|
"series": "სერია",
|
||||||
|
"series_count_header": "სერიები",
|
||||||
|
"series_plural": "სერიები",
|
||||||
|
"settings": {
|
||||||
|
"dicom_AET": "DICOM AET",
|
||||||
|
"dicom_port": "DICOM პორტი",
|
||||||
|
"ingest_transcoding": "შემოსული სურათების ტრანსკოდირება",
|
||||||
|
"installed_plugins": "დაინსტალირებული ფლაგინები",
|
||||||
|
"orthanc_name": "Orthanc სახელი",
|
||||||
|
"orthanc_system_info": "Orthanc სისტემის ინფორმაცია",
|
||||||
|
"orthanc_version": "Orthanc ვერსია",
|
||||||
|
"overwrite_instances": "სურათების გადაწერა",
|
||||||
|
"plugins_not_enabled": "ფლაგინები რომლებიც ჩაიტვირთა, მაგრამ არ არის ჩართული ან სწორად კონფიგურირებული: ",
|
||||||
|
"statistics": "სტატისტიკა",
|
||||||
|
"storage_compression": "ფაილების საცავის კომპრესია",
|
||||||
|
"storage_size": "ფაილების საცავის ზომა",
|
||||||
|
"striked_through": "გახაზულია",
|
||||||
|
"title": "პარამეტრები",
|
||||||
|
"verbosity_level": "ლოგირების დონე"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"anonymize": "მოხდეს ანონიმიზირება?",
|
||||||
|
"button_title": "გაზიარება",
|
||||||
|
"copy_and_close": "დაკოპირება და დახურვა",
|
||||||
|
"days": "დღეში",
|
||||||
|
"expires_in": "ვადა გასდის",
|
||||||
|
"link": "ბმული",
|
||||||
|
"modal_title": "კვლევის გაზიარება",
|
||||||
|
"never": "არასდროს"
|
||||||
|
},
|
||||||
|
"show_errors": "აჩვენე შეცდომები",
|
||||||
|
"studies": "კვლევები",
|
||||||
|
"study": "კვლევა",
|
||||||
|
"this_patient_has_no_other_studies": "ამ პაციენტს სხვა კვლევები არ აქვს.",
|
||||||
|
"this_patient_has_other_studies": "ამ პაციენტს ჯამში აქვს {count} კვლევა.",
|
||||||
|
"this_patient_has_other_studies_show": "ჩვენება",
|
||||||
|
"token": {
|
||||||
|
"error_token_expired_html": "თქვენი დაშვების კოდის ვადა ამოწურულია.",
|
||||||
|
"error_token_invalid_html": "თქვენი დაშვების კოდი არ არის ძალაში. <br/> გადაამოწმეთ რომ სწორად გაქვთ დაკოპირებული, ან დაეკონტაქტეთ ადამიანს ვინც გამოგიგზავნათ.",
|
||||||
|
"error_token_unknown_html": "თქვენი დაშვების კოდი არ არის ძალაში. <br/> გადაამოწმეთ რომ სწორად გაქვთ დაკოპირებული, ან დაეკონტაქტეთ ადამიანს ვინც გამოგიგზავნათ.",
|
||||||
|
"token_being_checked_html": "თქვენი დაშვების კოდი მოწმდება."
|
||||||
|
},
|
||||||
|
"trace": "ყველაზე დეტალური",
|
||||||
|
"upload": "ატვირთვა",
|
||||||
|
"uploaded_studies": "ატვირთული კვლევები",
|
||||||
|
"verbose": "ვრცელი",
|
||||||
|
"view_in_meddream": "MedDream Viewer-ში ნახვა",
|
||||||
|
"view_in_ohif": "OHIF Viewer-ში ნახვა",
|
||||||
|
"view_in_osimis": "OsimisViewer-ში ნახვა",
|
||||||
|
"view_in_stone": "StoneViewer-ში ნახვა",
|
||||||
|
"view_in_volview": "VolView-ში ნახვა"
|
||||||
|
}
|
||||||
213
WebApplication/src/locales/ru.json
Normal file
213
WebApplication/src/locales/ru.json
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
{
|
||||||
|
"all_modalities": "Все",
|
||||||
|
"anonymize": "Анонимизировать",
|
||||||
|
"cancel": "Отменить",
|
||||||
|
"change_password": "Изменить пароль",
|
||||||
|
"close": "Закрыть",
|
||||||
|
"copy_orthanc_id": "Копировать Orthanc ID ",
|
||||||
|
"date_picker": {
|
||||||
|
"last_12_months": "За последние 12 месяцев",
|
||||||
|
"last_month": "В прошлом месяце",
|
||||||
|
"last_week": "На прошлой неделе",
|
||||||
|
"this_month": "В этом месяце",
|
||||||
|
"this_week": "На этой неделе",
|
||||||
|
"today": "Сегодня",
|
||||||
|
"yesterday": "Вчера"
|
||||||
|
},
|
||||||
|
"default": "По умолчанию",
|
||||||
|
"delete": "Удалить",
|
||||||
|
"delete_instance_body": "Вы уверены, что хотите удалить этот экземпляр?<br/>Это действие нельзя отменить!",
|
||||||
|
"delete_instance_title": "Удалить экземпляр?",
|
||||||
|
"delete_series_body": "Вы уверены, что хотите удалить эту серию?<br/>Это действие нельзя отменить!",
|
||||||
|
"delete_series_title": "Удалить серию?",
|
||||||
|
"delete_studies_body": "Вы уверены, что хотите удалить эти исследования?<br/>Это действие нельзя отменить!",
|
||||||
|
"delete_studies_title": "Удалить исследования?",
|
||||||
|
"delete_study_body": "Вы уверены, что хотите удалить это исследование?<br/>Это действие нельзя отменить!",
|
||||||
|
"delete_study_title": "Удалить исследование?",
|
||||||
|
"description": "Описание",
|
||||||
|
"dicom_modalities": "Модальности DICOM",
|
||||||
|
"dicom_tags": {
|
||||||
|
"AccessionNumber": "Регистрационный номер",
|
||||||
|
"BodyPartExamined": "Обследованная часть тела",
|
||||||
|
"InstanceNumber": "Номер экземпляра",
|
||||||
|
"InstitutionName": "Название учреждения",
|
||||||
|
"ModalitiesInStudy": "Модальности в исследовании",
|
||||||
|
"Modality": "Модальность",
|
||||||
|
"NumberOfFrames": "Количество кадров",
|
||||||
|
"NumberOfStudyRelatedSeries": "Количество серий, связанных с исследованием",
|
||||||
|
"PatientBirthDate": "Дата рождения",
|
||||||
|
"PatientID": "ID пациента",
|
||||||
|
"PatientName": "Имя пациента",
|
||||||
|
"PatientSex": "Пол пациента",
|
||||||
|
"ProtocolName": "Название протокола",
|
||||||
|
"ReferringPhysicianName": "Направляющий врач",
|
||||||
|
"RequestingPhysician": "Запрашивающий врач",
|
||||||
|
"SOPInstanceUID": "UID экземпляра SOP",
|
||||||
|
"SeriesDate": "Дата серии",
|
||||||
|
"SeriesDescription": "Описание серии",
|
||||||
|
"SeriesInstanceUID": "UID экземпляра серии",
|
||||||
|
"SeriesNumber": "Номер серии",
|
||||||
|
"SeriesTime": "Время серии",
|
||||||
|
"StudyDate": "Дата исследования",
|
||||||
|
"StudyDescription": "Описание исследования",
|
||||||
|
"StudyID": "ID исследования",
|
||||||
|
"StudyInstanceUID": "UID экземпляра исследования",
|
||||||
|
"StudyTime": "Время исследования",
|
||||||
|
"seriesCount": "Количество серий"
|
||||||
|
},
|
||||||
|
"dicom_web_servers": "Серверы DICOM-Web",
|
||||||
|
"displaying_latest_studies": "Отображение последних исследований",
|
||||||
|
"download_dicom_file": "Загрузить файл DICOM",
|
||||||
|
"download_dicomdir": "Загрузить DICOMDIR",
|
||||||
|
"download_zip": "Загрузить ZIP-архив",
|
||||||
|
"drop_files": "Перетащите файлы сюда или",
|
||||||
|
"enter_search": "Введите критерии поиска для отображения результатов!",
|
||||||
|
"error": "Ошибка",
|
||||||
|
"file": "Файл",
|
||||||
|
"files": "файлы",
|
||||||
|
"frames": "кадры",
|
||||||
|
"instance": "Экземпляр",
|
||||||
|
"instances": "Экземпляры",
|
||||||
|
"instances_number": "Количество экземпляров",
|
||||||
|
"labels": {
|
||||||
|
"add_button": "Добавить",
|
||||||
|
"add_labels_message_html": "<strong>Добавить</strong> эти метки всем выбранным ресурсам",
|
||||||
|
"add_labels_placeholder": "Метки, которые нужно добавить. Нажмите Enter, чтобы создать новый",
|
||||||
|
"added_labels_message_part_1_html": "Добавленные метки ",
|
||||||
|
"added_labels_message_part_2_html": " до <strong>{count}</strong> исследований",
|
||||||
|
"clear_all_button": "Удалить все",
|
||||||
|
"clear_all_labels_message_html": "<strong>Удалить все</strong> метки во всех выделенных ресурсах",
|
||||||
|
"cleared_labels_message_part_1_html": "Удаленные метки ",
|
||||||
|
"cleared_labels_message_part_2_html": " из <strong>{count}</strong> исследований",
|
||||||
|
"edit_labels_button": "Редактировать метки",
|
||||||
|
"modal_title": "Редактирование меток в выделенных ресурсах",
|
||||||
|
"remove_button": "Удалить",
|
||||||
|
"remove_labels_message_html": "<strong>Удалить</strong> эти метки из всех выбранных ресурсов",
|
||||||
|
"remove_labels_placeholder": "Метки, которые будут удалены",
|
||||||
|
"removed_labels_message_part_1_html": "Удаленные метки ",
|
||||||
|
"removed_labels_message_part_2_html": " из <strong>{count}</strong> исследований",
|
||||||
|
"study_details_title": "Метки"
|
||||||
|
},
|
||||||
|
"legacy_ui": "Старый интерфейс",
|
||||||
|
"loading_latest_studies": "Загрузка последних исследований",
|
||||||
|
"local_studies": "Все локальные исследования",
|
||||||
|
"logout": "Выход",
|
||||||
|
"mammography_plugin": "Start mammogram AI",
|
||||||
|
"modalities_in_study": "Модальности в исследовании",
|
||||||
|
"modify": {
|
||||||
|
"anonymize_button_title": "Анонимизировать",
|
||||||
|
"anonymize_modal_title": "Анонимизировать",
|
||||||
|
"autogenerated_dicom_uid": "Автоматически сгенерированные",
|
||||||
|
"back_button_title": "Назад",
|
||||||
|
"error_attach_series_to_existing_study_target_does_not_exist_html": "Ошибка при смене серии, исследование с этим StudyInstanceUID не найдено.",
|
||||||
|
"error_attach_series_to_existing_study_target_unchanged_html": "Ошибка при смене серии, целевое исследование такое же, как текущее исследование серии.",
|
||||||
|
"error_attach_study_to_existing_patient_target_does_not_exist_html": "Ошибка при смене пациента, пациента с этим ID не найдено. Возможно, вместо этого нужно использовать<strong>Изменить теги исследования</strong>.",
|
||||||
|
"error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Ошибка при смене тегов исследования: этот пациент имеет другие исследования. Вместо этого следует использовать функцию<strong> Изменить теги пациента </strong>.",
|
||||||
|
"error_modify_any_study_tags_patient_exists_html": "Ошибка при смене тегов исследования: уже существует другой пациент с таким же ID. Вместо этого нужно использовать <strong>Изменить пациента</strong>.",
|
||||||
|
"error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Ошибка при смене тегов пациента: уже существует другой пациент с таким же ID. Вместо этого следует использовать функцию < strong > Изменить теги пациента </strong>.",
|
||||||
|
"error_modify_unexpected_error_html": "Неожиданная ошибка при модификации, проверьте журналы Orthanc",
|
||||||
|
"insert_tag": "Вставить тег DICOM",
|
||||||
|
"job_in_progress": "Модификация обрабатывается",
|
||||||
|
"modify_button_title": "Модифицировать",
|
||||||
|
"modify_modal_title": "Модифицировать",
|
||||||
|
"modify_study_mode_duplicate_html": "Создание модифицированной копии исследования. <small>(с генерацией новых DICOM UIDов и сохранением оригинального исследования)</small>",
|
||||||
|
"modify_study_mode_keep_uids_html": "Модификация оригинального исследования. <small>(с сохранением исходных DICOM UIDов)</small>",
|
||||||
|
"modify_study_mode_new_uids_html": "Модификация оригинального исследования. <small>(с генерацией новых DICOM UIDов)</small>",
|
||||||
|
"remove_tag_tooltip": "Удалить тег",
|
||||||
|
"remove_tag_undo_tooltip": "Отменить удаление тегов",
|
||||||
|
"series_step_0_attach_series_to_existing_study_button_title_html": "Изменить исследование",
|
||||||
|
"series_step_0_attach_series_to_existing_study_html": "Переместить эту серию в существующее исследование.",
|
||||||
|
"series_step_0_create_new_study_button_title_html": "Создать новое исследование",
|
||||||
|
"series_step_0_create_new_study_html": "Переместить эту серию в новое исследование.",
|
||||||
|
"series_step_0_modify_series_button_title_html": "Модифицировать теги серии",
|
||||||
|
"series_step_0_modify_series_html": "Модифицировать теги только в этой серии?",
|
||||||
|
"show_modified_resources": "Показать модифицированные ресурсы",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_button_title_html": "Сменить пациента",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_html": "Присоединить это исследование к другому пациенту.",
|
||||||
|
"study_step_0_modify_study_button_title_html": "Изменить теги исследования",
|
||||||
|
"study_step_0_modify_study_html": "Изменить теги пациента/исследования только в этом исследовании?",
|
||||||
|
"study_step_0_patient_has_other_studies_button_title_html": "Изменить теги пациента",
|
||||||
|
"study_step_0_patient_has_other_studies_html": "Общее количество исследований этого пациента: <strong> {count} </strong>. < br/> Вы хотите изменить пациента во всех этих исследованиях?",
|
||||||
|
"warning_create_new_study_from_series_html": "Уже существует пациент с другими тегами и с таким же ID. Хотите ли вы использовать существующие имя пациента, пол пациента и дату рождения пациента? "
|
||||||
|
},
|
||||||
|
"my_jobs": "Мои задачи",
|
||||||
|
"no_modalities": "Ничего",
|
||||||
|
"no_result_found": "Результатов нет!",
|
||||||
|
"not_showing_all_results": "Не показаны все результаты. Вы должны уточнить критерии поиска",
|
||||||
|
"open": "Открыть",
|
||||||
|
"page_not_found": "Такой страницы нет!",
|
||||||
|
"plugin": "дополнительный модуль",
|
||||||
|
"preview": "Предварительный просмотр",
|
||||||
|
"profile": "Профиль",
|
||||||
|
"retrieve_and_view": {
|
||||||
|
"finding_locally": "Проверка наличия исследования локально.",
|
||||||
|
"finding_remotely": "Поиск исследования в удаленных узлах DICOM.",
|
||||||
|
"not_found": "Исследование не было найдено ни локально, ни в удаленных узлах DICOM.",
|
||||||
|
"retrieved_html": "Получено <strong>{count}</strong> экземпляров.",
|
||||||
|
"retrieving": "Получение исследования."
|
||||||
|
},
|
||||||
|
"searching": "Поиск...",
|
||||||
|
"select_files": "Выберите файлы",
|
||||||
|
"select_folder": "Выберите папку",
|
||||||
|
"send_to": {
|
||||||
|
"button_title": "Отправить",
|
||||||
|
"dicom": "Отправить в узел DICOM",
|
||||||
|
"dicom_web": "Отправить на сервер DICOMWeb",
|
||||||
|
"orthanc_peer": "Отправить в другой Orthanc",
|
||||||
|
"transfers": "Отправить в другой Orthanc (расширенные параметры передачи)"
|
||||||
|
},
|
||||||
|
"series": "Серия",
|
||||||
|
"series_count_header": "# серий",
|
||||||
|
"series_plural": "Серии",
|
||||||
|
"settings": {
|
||||||
|
"dicom_AET": "DICOM AET",
|
||||||
|
"dicom_port": "Порт DICON",
|
||||||
|
"ingest_transcoding": "Транскодирование поступающих данных",
|
||||||
|
"installed_plugins": "Установлены дополнительные модули",
|
||||||
|
"orthanc_name": "Имя сервера",
|
||||||
|
"orthanc_system_info": "Информация о системе",
|
||||||
|
"orthanc_version": "Версия",
|
||||||
|
"overwrite_instances": "Заменить экземпляры",
|
||||||
|
"plugins_not_enabled": "Загруженные дополнительные модули, которые не включены или неверно настроены",
|
||||||
|
"statistics": "Статистика",
|
||||||
|
"storage_compression": "Сжатие хранилища",
|
||||||
|
"storage_size": "Размер хранилища",
|
||||||
|
"striked_through": "зачеркнутые",
|
||||||
|
"title": "Настройка",
|
||||||
|
"verbosity_level": "Уровень подробности"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"anonymize": "Анонимизировать?",
|
||||||
|
"button_title": "Поделиться",
|
||||||
|
"copy_and_close": "Копировать и закрыть",
|
||||||
|
"days": "дни",
|
||||||
|
"expires_in": "Срок действия заканчивается через",
|
||||||
|
"link": "Ссылка",
|
||||||
|
"modal_title": "Поделиться исследованием",
|
||||||
|
"never": "никогда"
|
||||||
|
},
|
||||||
|
"show_errors": "Показать ошибки",
|
||||||
|
"studies": "Исследования",
|
||||||
|
"study": "Исследование",
|
||||||
|
"this_patient_has_no_other_studies": "У этого пациента нет больше исследований.",
|
||||||
|
"this_patient_has_other_studies": "У этого пациента в целом {count} исследований.",
|
||||||
|
"this_patient_has_other_studies_show": "Показать их!",
|
||||||
|
"token": {
|
||||||
|
"error_token_expired_html": "Ваш токен просрочен.",
|
||||||
|
"error_token_invalid_html": "Ваш токен недействителен. <br/> Проверьте, вставили ли его полностью, или обратитесь к человеку, который вам его предоставил.",
|
||||||
|
"error_token_unknown_html": "Ваш токен недействителен. <br/> Проверьте, вставили ли его полностью, или обратитесь к человеку, который вам его предоставил.",
|
||||||
|
"token_being_checked_html": "Ваш токен проверяется."
|
||||||
|
},
|
||||||
|
"trace": "Трассировка",
|
||||||
|
"upload": "Загрузить",
|
||||||
|
"uploaded_studies": "Загруженные исследования",
|
||||||
|
"verbose": "Подробнее",
|
||||||
|
"view_in_meddream": "Посмотреть в MedDream",
|
||||||
|
"view_in_ohif": "Посмотреть в OHIF",
|
||||||
|
"view_in_ohif_tmtv": "Посмотреть в OHIF в режиме Total Metabolic Tumor Volume",
|
||||||
|
"view_in_ohif_vr": "Посмотреть в OHIF в режиме Volume Rendering",
|
||||||
|
"view_in_osimis": "Посмотреть в OsimisViewer",
|
||||||
|
"view_in_stone": "Посмотреть в StoneViewer",
|
||||||
|
"view_in_volview": "Посмотреть в VolView",
|
||||||
|
"view_in_wsi_viewer": "Посмотреть в Whole Slide Imaging Viewer"
|
||||||
|
}
|
||||||
217
WebApplication/src/locales/si.json
Normal file
217
WebApplication/src/locales/si.json
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
{
|
||||||
|
"all_modalities": "Vse",
|
||||||
|
"anonymize": "Anonimiziraj",
|
||||||
|
"cancel": "Prekliči",
|
||||||
|
"change_password": "Spremeni geslo",
|
||||||
|
"close": "Zapri",
|
||||||
|
"copy_orthanc_id": "Kopiraj Orthanc ID",
|
||||||
|
"date_picker": {
|
||||||
|
"last_12_months": "Zadnjih 12 mesecev",
|
||||||
|
"last_month": "Prejšnji mesec",
|
||||||
|
"last_week": "Prejšnji teden",
|
||||||
|
"this_month": "Ta mesec",
|
||||||
|
"this_week": "Ta teden",
|
||||||
|
"today": "Danes",
|
||||||
|
"yesterday": "Včeraj"
|
||||||
|
},
|
||||||
|
"default": "Privzeto",
|
||||||
|
"delete": "Izbriši",
|
||||||
|
"delete_instance_body": "Ste prepričani, da želite izbrisati ta primerek?<br/>Tega dejanja se ne da razveljaviti!",
|
||||||
|
"delete_instance_title": "Izbriši primerek?",
|
||||||
|
"delete_series_body": "Ste prepričani, da želite izbrisati to serijo?<br/>Tega dejanja se ne da razveljaviti!",
|
||||||
|
"delete_series_title": "Izbriši serijo?",
|
||||||
|
"delete_studies_body": "Ste prepričani, da želite izbrisati te študije?<br/>Tega dejanja se ne da razveljaviti!",
|
||||||
|
"delete_studies_title": "Izbriši študije?",
|
||||||
|
"delete_study_body": "Ste prepričani, da želite izbrisati to študijo?<br/>Tega dejanja se ne da razveljaviti!",
|
||||||
|
"delete_study_title": "Izbriši študijo?",
|
||||||
|
"description": "Opis",
|
||||||
|
"dicom_modalities": "DICOM modalnosti",
|
||||||
|
"dicom_tags": {
|
||||||
|
"AccessionNumber": "Pristopna številka",
|
||||||
|
"BodyPartExamined": "Pregledan del telesa",
|
||||||
|
"InstanceNumber": "Številka primerka",
|
||||||
|
"InstitutionName": "Ime ustanove",
|
||||||
|
"ModalitiesInStudy": "Modalitete v študiji",
|
||||||
|
"Modality": "Modalnost",
|
||||||
|
"NumberOfFrames": "Število slik",
|
||||||
|
"NumberOfStudyRelatedSeries": "Število serij",
|
||||||
|
"PatientBirthDate": "Datum rojstva pacienta",
|
||||||
|
"PatientID": "ID pacienta",
|
||||||
|
"PatientName": "Ime pacienta",
|
||||||
|
"PatientSex": "Spol pacienta",
|
||||||
|
"ProtocolName": "Ime protokola",
|
||||||
|
"ReferringPhysicianName": "Ime napotnega zdravnika",
|
||||||
|
"RequestingPhysician": "Zdravnik prosilec",
|
||||||
|
"SOPInstanceUID": "UID primerka SOP",
|
||||||
|
"SeriesDate": "Datum serije",
|
||||||
|
"SeriesDescription": "Opis serije",
|
||||||
|
"SeriesInstanceUID": "UID primerka serije",
|
||||||
|
"SeriesNumber": "Številka serije",
|
||||||
|
"SeriesTime": "Čas serije",
|
||||||
|
"StudyDate": "Datum študije",
|
||||||
|
"StudyDescription": "Opis študije",
|
||||||
|
"StudyID": "ID študije",
|
||||||
|
"StudyInstanceUID": "UID primerka študije",
|
||||||
|
"StudyTime": "Čas študije",
|
||||||
|
"seriesCount": "Število serij"
|
||||||
|
},
|
||||||
|
"dicom_web_servers": "DICOM-Web strežnik",
|
||||||
|
"displaying_latest_studies": "Prikaz najnovejših študij",
|
||||||
|
"download_dicom_file": "Prenos datoteke DICOM",
|
||||||
|
"download_dicomdir": "Prenesi datoteko DICOMDIR",
|
||||||
|
"download_zip": "Prenesi datoteko ZIP",
|
||||||
|
"drop_files": "Datoteke odvrzi sem ali pa...",
|
||||||
|
"enter_search": "Vnesi kriterij za iskanje za prikaz rezultatov!",
|
||||||
|
"error": "Napaka",
|
||||||
|
"file": "Datoteka",
|
||||||
|
"files": "datoteke",
|
||||||
|
"frames": "slike",
|
||||||
|
"instance": "Primerek",
|
||||||
|
"instances": "Primerki",
|
||||||
|
"instances_number": "Število primerkov",
|
||||||
|
"labels": {
|
||||||
|
"add_button": "Dodaj",
|
||||||
|
"add_labels_message_html": "<strong>Dodajte</strong> te oznake vsem izbranim virom",
|
||||||
|
"add_labels_placeholder": "Oznake, ki jih želite dodati. Pritisnite Enter, da ustvarite novo oznako.",
|
||||||
|
"added_labels_message_part_1_html": "Dodane oznake",
|
||||||
|
"added_labels_message_part_2_html": " do <strong>{count}</strong> študij",
|
||||||
|
"clear_all_button": "Odstrani vse",
|
||||||
|
"clear_all_labels_message_html": "<strong>Odstrani vse</strong> oznake v vseh izbranih virih",
|
||||||
|
"cleared_labels_message_part_1_html": "Odstranjene oznake",
|
||||||
|
"cleared_labels_message_part_2_html": " iz <strong>{count}</strong> študij",
|
||||||
|
"edit_labels_button": "Uredi oznake",
|
||||||
|
"modal_title": "Spremeni oznake v izbranih virih",
|
||||||
|
"remove_button": "Odstrani",
|
||||||
|
"remove_labels_message_html": "<strong>Odstranite</strong> te oznake iz vseh izbranih virov",
|
||||||
|
"remove_labels_placeholder": "Oznake za odstranitev",
|
||||||
|
"removed_labels_message_part_1_html": "Odstranjene oznake",
|
||||||
|
"removed_labels_message_part_2_html": " iz <strong>{count}</strong> študij",
|
||||||
|
"study_details_title": "Oznake"
|
||||||
|
},
|
||||||
|
"legacy_ui": "Starejši uporabniški vmesnik",
|
||||||
|
"loading_latest_studies": "Nalaganje najnovejših študij",
|
||||||
|
"local_studies": "Vse lokalne študije",
|
||||||
|
"logout": "Odjava",
|
||||||
|
"mammography_plugin": "Start mammogram AI",
|
||||||
|
"modalities_in_study": "Modalitete v študiju",
|
||||||
|
"modify": {
|
||||||
|
"anonymize_button_title": "Anonimiziraj",
|
||||||
|
"anonymize_modal_title": "Anonimiziraj",
|
||||||
|
"autogenerated_dicom_uid": "Samodejno ustvarjeno",
|
||||||
|
"back_button_title": "Nazaj",
|
||||||
|
"error_attach_series_to_existing_study_target_does_not_exist_html": "Napaka pri spreminjanju serije, s tem \"StudyInstanceUID\" ni bilo mogoče najti študije.",
|
||||||
|
"error_attach_series_to_existing_study_target_unchanged_html": "Napaka pri spreminjanju serije, ciljna študija je enaka trenutni študiji serije.",
|
||||||
|
"error_attach_study_to_existing_patient_target_does_not_exist_html": "Napaka pri menjavi pacienta. Pacient s tem ID-jem ne obstaja. Poizkusite s <strong>Spremenite študijske oznake</strong> funkcijo.",
|
||||||
|
"error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Napaka pri spreminjanju oznak študije: ta pacient ima druge študije. Namesto tega uporabite funkcijo <strong>Spremenite oznake pacienta</strong>.",
|
||||||
|
"error_modify_any_study_tags_patient_exists_html": "Napaka pri spreminjanju oznak študije: pacient s tem ID-jem že obstaja. Namesto tega uporabite funkcijo <strong>Spremeni pacienta/strong>.",
|
||||||
|
"error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Napaka pri spreminjanju oznak pacienta: drug pacient z istim ID-jem že obstaja. Namesto tega uporabite funkcijo <strong>Spremenite oznake pacienta</strong>.",
|
||||||
|
"error_modify_unexpected_error_html": "Nepričakovana napaka med spreminjanjem, preverite Orthanc-ove dnevnike",
|
||||||
|
"insert_tag": "Vstavi DICOM oznako",
|
||||||
|
"job_in_progress": "Sprememba je v obdelavi",
|
||||||
|
"modify_button_title": "Spremeni",
|
||||||
|
"modify_modal_title": "Spremeni",
|
||||||
|
"modify_series_mode_duplicate_html": "Ustvari spremenjeno kopijo izvirne serije. <small>(ustvarjanje novih DICOM UID-jev in ohranjanje izvirne serije)</small>",
|
||||||
|
"modify_series_mode_keep_uids_html": "Popravi izvirno serijo. <small>(ohranjanje izvirnih DICOM UID-jev)</small>",
|
||||||
|
"modify_series_mode_new_uids_html": "Spremeni izvirno serijo. <small>(ustvarjanje novih DICOM UID-jev)</small>",
|
||||||
|
"modify_study_mode_duplicate_html": "Ustvari spremenjeno kopijo izvirne študije. <small>(ustvarjanje novih DICOM UID-jev in ohranjanje izvirne študije)</small>",
|
||||||
|
"modify_study_mode_keep_uids_html": "Popravi izvirno študijo. <small>(ohranjanje izvirnih DICOM UID-jev)</small>",
|
||||||
|
"modify_study_mode_new_uids_html": "Spremeni izvirno študijo. <small>(ustvarjanje novih DICOM UID-jev)</small>",
|
||||||
|
"remove_tag_tooltip": "Odstrani oznako",
|
||||||
|
"remove_tag_undo_tooltip": "Razveljavi odstranitev oznake",
|
||||||
|
"series_step_0_attach_series_to_existing_study_button_title_html": "Spremeni študijo",
|
||||||
|
"series_step_0_attach_series_to_existing_study_html": "Premakni to serijo v obstoječo študijo.",
|
||||||
|
"series_step_0_create_new_study_button_title_html": "Ustvari novo študijo",
|
||||||
|
"series_step_0_create_new_study_html": "Premakni to serijo v novo študijo.",
|
||||||
|
"series_step_0_modify_series_button_title_html": "Spremeni oznake serije",
|
||||||
|
"series_step_0_modify_series_html": "Želite spremeniti oznake serije samo v tej seriji?",
|
||||||
|
"show_modified_resources": "Pokaži spremenjene vire",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_button_title_html": "Spremeni pacienta",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_html": "Priloži to študijo drugemu obstoječemu pacientu.",
|
||||||
|
"study_step_0_modify_study_button_title_html": "Spremeni oznake študije",
|
||||||
|
"study_step_0_modify_study_html": "Želite spremeniti oznake pacienta/študije samo v tej študiji?",
|
||||||
|
"study_step_0_patient_has_other_studies_button_title_html": "Spremeni oznake pacientov",
|
||||||
|
"study_step_0_patient_has_other_studies_html": "Ta pacient ima skupno <strong>{count}</strong> študij.<br/>Ali želite spremeniti pacienta v vseh teh študijah?",
|
||||||
|
"warning_create_new_study_from_series_html": "Pacient z istim ID-jem, ampak drugačnimi oznakami, že obstaja. Ali želite uporabiti obstoječe ime pacienta, njegov spol in datum rojstva?"
|
||||||
|
},
|
||||||
|
"my_jobs": "Moja opravila",
|
||||||
|
"no_modalities": "Noben",
|
||||||
|
"no_result_found": "Ni rezultatov!",
|
||||||
|
"not_showing_all_results": "Niso prikazani vsi rezultati. Uporabite bolj specifične iskalne kriterije",
|
||||||
|
"open": "Odprto",
|
||||||
|
"page_not_found": "Stran ni najdena!",
|
||||||
|
"plugin": "vtičnik",
|
||||||
|
"preview": "Predogled",
|
||||||
|
"profile": "Profil",
|
||||||
|
"retrieve_and_view": {
|
||||||
|
"finding_locally": "Preverjam, ali je študija že na voljo lokalno.",
|
||||||
|
"finding_remotely": "Iskanje študije v oddaljenih DICOM vozliščih.",
|
||||||
|
"not_found": "Študija ni bila najdena niti lokalno niti v oddaljenih DICOM vozliščih.",
|
||||||
|
"retrieved_html": "Pridobljenih <strong>{count}</strong> primerkov.",
|
||||||
|
"retrieving": "Pridobivanje študije."
|
||||||
|
},
|
||||||
|
"searching": "Iščem ...",
|
||||||
|
"select_files": "Izberi datoteke",
|
||||||
|
"select_folder": "Izberi mapo",
|
||||||
|
"send_to": {
|
||||||
|
"button_title": "Pošlji",
|
||||||
|
"dicom": "Pošlji v DICOM vozlišče",
|
||||||
|
"dicom_web": "Pošlji na DICOMWeb strežnik",
|
||||||
|
"orthanc_peer": "Pošlji Orthanc vrstniku",
|
||||||
|
"transfers": "Pošlji Orthanc vrstniku (napredni prenosi)"
|
||||||
|
},
|
||||||
|
"series": "Serija",
|
||||||
|
"series_count_header": "# serij",
|
||||||
|
"series_plural": "Serije",
|
||||||
|
"settings": {
|
||||||
|
"dicom_AET": "DICOM AET",
|
||||||
|
"dicom_port": "DICOM vrata",
|
||||||
|
"ingest_transcoding": "Prevzemno prekodiranje",
|
||||||
|
"installed_plugins": "Nameščeni vtičniki",
|
||||||
|
"orthanc_name": "Ime Orthanc",
|
||||||
|
"orthanc_system_info": "Informacije o sistemu Orthanc",
|
||||||
|
"orthanc_version": "Različica Orthanc",
|
||||||
|
"overwrite_instances": "Prepiši primerke",
|
||||||
|
"plugins_not_enabled": "Vtičniki, ki so naloženi, vendar niso omogočeni ali niso pravilno konfigurirani, so",
|
||||||
|
"statistics": "Statistika",
|
||||||
|
"storage_compression": "Stiskanje shranjevanja",
|
||||||
|
"storage_size": "Velikost shranjevanja",
|
||||||
|
"striked_through": "prečrtani",
|
||||||
|
"title": "Nastavitve",
|
||||||
|
"verbosity_level": "Nivo beleženja"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"anonymize": "Anonimiziraj?",
|
||||||
|
"button_title": "Deli",
|
||||||
|
"copy_and_close": "Kopiraj in zapri",
|
||||||
|
"days": "dni",
|
||||||
|
"expires_in": "Poteče čez",
|
||||||
|
"link": "Povezava",
|
||||||
|
"modal_title": "Deli študijo",
|
||||||
|
"modal_title_multiple_studies": "Deli {count} študij",
|
||||||
|
"never": "nikoli"
|
||||||
|
},
|
||||||
|
"show_errors": "Pokaži napake",
|
||||||
|
"studies": "Študije",
|
||||||
|
"study": "Študija",
|
||||||
|
"this_patient_has_no_other_studies": "Ta pacient nima drugih študij.",
|
||||||
|
"this_patient_has_other_studies": "Ta pacient ima skupno {count} študij.",
|
||||||
|
"this_patient_has_other_studies_show": "Prikaži jih",
|
||||||
|
"token": {
|
||||||
|
"error_token_expired_html": "Vaš žeton je potekel.",
|
||||||
|
"error_token_invalid_html": "Vaš žeton ni veljaven.<br/>Preverite, ali ste ga prilepili v celoti, ali pa se obrnite na osebo, ki vam ga je posredovala.",
|
||||||
|
"error_token_unknown_html": "Vaš žeton ni veljaven.<br/>Preverite, ali ste ga prilepili v celoti, ali pa se obrnite na osebo, ki vam ga je posredovala.",
|
||||||
|
"token_being_checked_html": "Vaš žeton se preverja."
|
||||||
|
},
|
||||||
|
"trace": "Napredno",
|
||||||
|
"upload": "Naloži",
|
||||||
|
"uploaded_studies": "Naložene študije",
|
||||||
|
"verbose": "Podrobno",
|
||||||
|
"view_in_meddream": "Ogled v MedDream",
|
||||||
|
"view_in_ohif": "Ogled v OHIF",
|
||||||
|
"view_in_ohif_tmtv": "Ogled v načinu OHIF Total Metabolic Tumor Volume",
|
||||||
|
"view_in_ohif_vr": "Ogled v načinu OHIF Volume Rendering",
|
||||||
|
"view_in_osimis": "Ogled v OsimisViewer",
|
||||||
|
"view_in_stone": "Ogled v StoneViewer",
|
||||||
|
"view_in_volview": "Ogled v VolView",
|
||||||
|
"view_in_wsi_viewer": "Ogled v Whole Slide Imaging Viewer"
|
||||||
|
}
|
||||||
214
WebApplication/src/locales/uk.json
Normal file
214
WebApplication/src/locales/uk.json
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
{
|
||||||
|
"all_modalities": "Всі",
|
||||||
|
"anonymize": "Анонімізувати",
|
||||||
|
"cancel": "Скасувати",
|
||||||
|
"change_password": "Змінити пароль",
|
||||||
|
"close": "Закрити",
|
||||||
|
"copy_orthanc_id": "Копіювати Orthanc ID",
|
||||||
|
"date_picker": {
|
||||||
|
"last_12_months": "За останні 12 місяців",
|
||||||
|
"last_month": "Минулого місяця",
|
||||||
|
"last_week": "Минулого тижня",
|
||||||
|
"this_month": "Цього місяця",
|
||||||
|
"this_week": "Цього тижня",
|
||||||
|
"today": "Сьогодні",
|
||||||
|
"yesterday": "Вчора"
|
||||||
|
},
|
||||||
|
"default": "За замовчуванням",
|
||||||
|
"delete": "Видалити",
|
||||||
|
"delete_instance_body": "Ви впевнені, що хочете видалити цей екземпляр?<br/>Цю дію не можна скасувати!",
|
||||||
|
"delete_instance_title": "Видалити екземпляр?",
|
||||||
|
"delete_series_body": "Ви впевнені, що хочете видалити ці серії?<br/>Цю дію не можна скасувати!",
|
||||||
|
"delete_series_title": "Видалити серії?",
|
||||||
|
"delete_studies_body": "Ви впевнені, що хочете видалити ці дослідження?<br/>Цю дію не можна скасувати!",
|
||||||
|
"delete_studies_title": "Видалити дослідження?",
|
||||||
|
"delete_study_body": "Ви впевнені, що хочете видалити це дослідження?<br/>Цю дію не можна скасувати!",
|
||||||
|
"delete_study_title": "Видалити дослідження?",
|
||||||
|
"description": "Опис",
|
||||||
|
"dicom_modalities": "Модальності DICOM",
|
||||||
|
"dicom_tags": {
|
||||||
|
"AccessionNumber": "Номер обстеження",
|
||||||
|
"BodyPartExamined": "Обстежена частина тіла",
|
||||||
|
"InstanceNumber": "Номер екземпляра",
|
||||||
|
"InstitutionName": "Назва установи",
|
||||||
|
"ModalitiesInStudy": "Модальності у дослідженні",
|
||||||
|
"Modality": "Модальність",
|
||||||
|
"NumberOfFrames": "Кількість кадрів",
|
||||||
|
"NumberOfStudyRelatedSeries": "Кількість серій, пов'язаних із дослідженням",
|
||||||
|
"PatientBirthDate": "Дата народження пацієнта",
|
||||||
|
"PatientID": "ID пацієнта",
|
||||||
|
"PatientName": "Ім'я пацієнта",
|
||||||
|
"PatientSex": "Стать пацієнта",
|
||||||
|
"ProtocolName": "Назва протоколу",
|
||||||
|
"ReferringPhysicianName": "Ім'я лікаря, що направив",
|
||||||
|
"RequestingPhysician": "Лікар, що робить запит",
|
||||||
|
"SOPInstanceUID": "UID екземпляра SOP",
|
||||||
|
"SeriesDate": "Дата серії",
|
||||||
|
"SeriesDescription": "Опис серії",
|
||||||
|
"SeriesInstanceUID": "UID екземпляра серії",
|
||||||
|
"SeriesNumber": "Номер серії",
|
||||||
|
"SeriesTime": "Час серії",
|
||||||
|
"StudyDate": "Дата дослідження",
|
||||||
|
"StudyDescription": "Опис дослідження",
|
||||||
|
"StudyID": "ID дослідження",
|
||||||
|
"StudyInstanceUID": "UID екземпляра дослідження",
|
||||||
|
"StudyTime": "Час дослідження",
|
||||||
|
"seriesCount": "Кількість серій"
|
||||||
|
},
|
||||||
|
"dicom_web_servers": "Сервери DICOM-Web",
|
||||||
|
"displaying_latest_studies": "Відображення останніх досліджень",
|
||||||
|
"download_dicom_file": "Завантажити файл DICOM",
|
||||||
|
"download_dicomdir": "Завантажити DICOMDIR",
|
||||||
|
"download_zip": "Завантажити ZIP-архів",
|
||||||
|
"drop_files": "Перетягніть файли сюди або",
|
||||||
|
"enter_search": "Введіть критерії пошуку, щоб відобразити результати!",
|
||||||
|
"error": "Помилка",
|
||||||
|
"file": "Файл",
|
||||||
|
"files": "файли",
|
||||||
|
"frames": "кадри",
|
||||||
|
"instance": "Екземпляр",
|
||||||
|
"instances": "Екземпляри",
|
||||||
|
"instances_number": "Кількість екземплярів",
|
||||||
|
"labels": {
|
||||||
|
"add_button": "Додати",
|
||||||
|
"add_labels_message_html": "<strong>Додати</strong> ці мітки усім вибраним ресурсам",
|
||||||
|
"add_labels_placeholder": "Мітки, що будуть додані; натисність Enter для створення нової",
|
||||||
|
"added_labels_message_part_1_html": "Додані мітки ",
|
||||||
|
"added_labels_message_part_2_html": " до <strong>{count}</strong> досліджень",
|
||||||
|
"clear_all_button": "Видалити все",
|
||||||
|
"clear_all_labels_message_html": "<strong>Видалити всі</strong> мітки у всіх виділених ресурсах",
|
||||||
|
"cleared_labels_message_part_1_html": "Видалені мітки ",
|
||||||
|
"cleared_labels_message_part_2_html": " з <strong>{count}</strong> досліджень",
|
||||||
|
"edit_labels_button": "Редагувати мітки",
|
||||||
|
"modal_title": "Редагування міток у виділених ресурсах",
|
||||||
|
"remove_button": "Видалити",
|
||||||
|
"remove_labels_message_html": "<strong>Видалити</strong> ці мітки з усіх вибраних ресурсів",
|
||||||
|
"remove_labels_placeholder": "Мітки, що будуть вилучені",
|
||||||
|
"removed_labels_message_part_1_html": "Видалені мітки ",
|
||||||
|
"removed_labels_message_part_2_html": " з <strong>{count}</strong> досліджень",
|
||||||
|
"study_details_title": "Мітки"
|
||||||
|
},
|
||||||
|
"legacy_ui": "Старий інтерфейс",
|
||||||
|
"loading_latest_studies": "Завантаження останніх досліджень",
|
||||||
|
"local_studies": "Усі локальні дослідження",
|
||||||
|
"logout": "Вихід",
|
||||||
|
"mammography_plugin": "Start mammogram AI",
|
||||||
|
"modalities_in_study": "Модальності у дослідженні",
|
||||||
|
"modify": {
|
||||||
|
"anonymize_button_title": "Анонімізувати",
|
||||||
|
"anonymize_modal_title": "Анонімізувати",
|
||||||
|
"autogenerated_dicom_uid": "Автоматично згенеровані",
|
||||||
|
"back_button_title": "Взад",
|
||||||
|
"error_attach_series_to_existing_study_target_does_not_exist_html": "Помилка під час зміни серії, дослідження з цим StudyInstanceUID не знайдено.",
|
||||||
|
"error_attach_series_to_existing_study_target_unchanged_html": "Помилка під час зміни серії, цільове дослідження таке ж, як поточне дослідження серії.",
|
||||||
|
"error_attach_study_to_existing_patient_target_does_not_exist_html": "Помилка під час зміни пацієнта, пацієнта з цим ID пацієнта не знайдено. Можливо, замість цього вам слід скористатися функцією <strong>Змінити теги дослідження</strong>.",
|
||||||
|
"error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "Помилка під час зміни тегів дослідження: цей пацієнт має інші дослідження. Замість цього слід використовувати функцію <strong>Змінити теги пацієнта</strong>.",
|
||||||
|
"error_modify_any_study_tags_patient_exists_html": "Помилка під час зміни тегів дослідження: уже існує інший пацієнт із таким самим ID пацієнта. Замість цього слід скористатися функцією <strong>Змінити пацієнта</strong>.",
|
||||||
|
"error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "Помилка під час зміни тегів пацієнта: уже існує інший пацієнт із таким самим ID пацієнта. Замість цього слід використовувати функцію <strong>Змінити теги пацієнта</strong>.",
|
||||||
|
"error_modify_unexpected_error_html": "Неочікувана помилка під час модифікації, перевірте журнали Orthanc",
|
||||||
|
"insert_tag": "Вставити тег DICOM",
|
||||||
|
"job_in_progress": "Модифікація обробляється",
|
||||||
|
"modify_button_title": "Модифікувати",
|
||||||
|
"modify_modal_title": "Модифікувати",
|
||||||
|
"modify_study_mode_duplicate_html": "Створення модифікованої копії дослідження. <small>(з генерацією нових DICOM UIDів ТА збереженням оригінального дослідження)</small>",
|
||||||
|
"modify_study_mode_keep_uids_html": "Модифікація оригінального дослідження. <small>(зі збереженням оригінальних DICOM UIDів)</small>",
|
||||||
|
"modify_study_mode_new_uids_html": "Модифікація оригінального дослідження. <small>(з генерацією нових DICOM UIDів)</small>",
|
||||||
|
"remove_tag_tooltip": "Видалити тег",
|
||||||
|
"remove_tag_undo_tooltip": "Відмінити видалення тегу",
|
||||||
|
"series_step_0_attach_series_to_existing_study_button_title_html": "Змінити дослідження",
|
||||||
|
"series_step_0_attach_series_to_existing_study_html": "Перемістіти цю серію до наявного дослідження.",
|
||||||
|
"series_step_0_create_new_study_button_title_html": "Створити нове дослідження",
|
||||||
|
"series_step_0_create_new_study_html": "Перемістити цю серію до нового дослідження.",
|
||||||
|
"series_step_0_modify_series_button_title_html": "Модифікувати теги серії",
|
||||||
|
"series_step_0_modify_series_html": "Модифікувати теги серії лише у цій серії?",
|
||||||
|
"show_modified_resources": "Показати модифіковані ресурси",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_button_title_html": "Змінити пацієнта",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_html": "Приєднати це дослідження до іншого наявного пацієнта.",
|
||||||
|
"study_step_0_modify_study_button_title_html": "Змінити теги дослідження",
|
||||||
|
"study_step_0_modify_study_html": "Змінити теги пацієнта/дослідження лише в цьому дослідженні?",
|
||||||
|
"study_step_0_patient_has_other_studies_button_title_html": "Змінити теги пацієнта",
|
||||||
|
"study_step_0_patient_has_other_studies_html": "Загальна кількість досліджень цього пацієнта: <strong>{count}</strong>.<br/>Ви бажаєте змінити пацієнта в усіх цих дослідженнях ?",
|
||||||
|
"warning_create_new_study_from_series_html": "Вже існує пацієнт із іншими тегами і з таким самим ID пацієнта. Чи бажаєте ви використовувати наявні ім'я пацієнта, стать пацієнта і дату народження пацієнта?"
|
||||||
|
},
|
||||||
|
"my_jobs": "Мої завдання",
|
||||||
|
"no_modalities": "Нічого",
|
||||||
|
"no_result_found": "Результатів немає!",
|
||||||
|
"not_showing_all_results": "Не показано всі результати. Ви повинні уточнити критерії пошуку",
|
||||||
|
"open": "Відкрити",
|
||||||
|
"page_not_found": "Такої сторінки немає!",
|
||||||
|
"plugin": "додатковий модуль",
|
||||||
|
"preview": "Попередній перегляд",
|
||||||
|
"profile": "Профіль",
|
||||||
|
"retrieve_and_view": {
|
||||||
|
"finding_locally": "Перевірка наявності дослідження локально.",
|
||||||
|
"finding_remotely": "Пошук дослідження у віддалених вузлах DICOM.",
|
||||||
|
"not_found": "Дослідження не було знайдено ані локально, ані у віддалених вузлах DICOM.",
|
||||||
|
"retrieved_html": "Отримано <strong>{count}</strong> екземплярів.",
|
||||||
|
"retrieving": "Отримання дослідження."
|
||||||
|
},
|
||||||
|
"searching": "Пошук...",
|
||||||
|
"select_files": "Виберіть файли",
|
||||||
|
"select_folder": "Виберіть теку",
|
||||||
|
"send_to": {
|
||||||
|
"button_title": "Надіслати",
|
||||||
|
"dicom": "Надіслати до вузла DICOM",
|
||||||
|
"dicom_web": "Надіслати до сервера DICOMWeb",
|
||||||
|
"orthanc_peer": "Надіслати до іншого Orthanc",
|
||||||
|
"transfers": "Надіслати до іншого Orthanc (розширені параметри передавання)"
|
||||||
|
},
|
||||||
|
"series": "Серія",
|
||||||
|
"series_count_header": "# серій",
|
||||||
|
"series_plural": "Серії",
|
||||||
|
"settings": {
|
||||||
|
"dicom_AET": "DICOM AE Title",
|
||||||
|
"dicom_port": "Порт DICOM",
|
||||||
|
"ingest_transcoding": "Транскодування даних, що надходять",
|
||||||
|
"installed_plugins": "Встановлені додаткові модулі",
|
||||||
|
"orthanc_name": "Ім'я сервера Orthanc",
|
||||||
|
"orthanc_system_info": "Інформація про систему Orthanc",
|
||||||
|
"orthanc_version": "Версія Orthanc",
|
||||||
|
"overwrite_instances": "Перезаписати екземпляри",
|
||||||
|
"plugins_not_enabled": "Завантажені додаткові модулі, які не ввімкнені або некоректно налаштовані, ",
|
||||||
|
"statistics": "Статистика",
|
||||||
|
"storage_compression": "Стиснення сховища",
|
||||||
|
"storage_size": "Розмір сховища",
|
||||||
|
"striked_through": "закреслені",
|
||||||
|
"title": "Налаштування",
|
||||||
|
"verbosity_level": "Рівень докладності"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"anonymize": "Анонімізувати?",
|
||||||
|
"button_title": "Поділитись",
|
||||||
|
"copy_and_close": "Копіювати та закрити",
|
||||||
|
"days": "дні",
|
||||||
|
"expires_in": "Строк дії закінчується через",
|
||||||
|
"link": "Посилання",
|
||||||
|
"modal_title": "Поділитись дослідженням",
|
||||||
|
"modal_title_multiple_studies": "Поділитись {count} дослідженнями",
|
||||||
|
"never": "ніколи"
|
||||||
|
},
|
||||||
|
"show_errors": "Показати помилки",
|
||||||
|
"studies": "Дослідження",
|
||||||
|
"study": "Дослідження",
|
||||||
|
"this_patient_has_no_other_studies": "У цього пацієнта немає більше досліджень.",
|
||||||
|
"this_patient_has_other_studies": "У цього пацієнта загалом {count} досліджень.",
|
||||||
|
"this_patient_has_other_studies_show": "Показати їх!",
|
||||||
|
"token": {
|
||||||
|
"error_token_expired_html": "Ваш токен прострочений.",
|
||||||
|
"error_token_invalid_html": "Ваш токен недійсний.<br/>Перевірте, чи вставили його повністю, або зверніться до особи, яка вам його надала.",
|
||||||
|
"error_token_unknown_html": "Ваш токен недійсний.<br/>Перевірте, чи вставили його повністю, або зверніться до особи, яка вам його надала.",
|
||||||
|
"token_being_checked_html": "Ваш токен перевіряється."
|
||||||
|
},
|
||||||
|
"trace": "Трасування",
|
||||||
|
"upload": "Завантажити",
|
||||||
|
"uploaded_studies": "Завантажені дослідження",
|
||||||
|
"verbose": "Докладно",
|
||||||
|
"view_in_meddream": "Подивитись у MedDream",
|
||||||
|
"view_in_ohif": "Подивитися в OHIF",
|
||||||
|
"view_in_ohif_tmtv": "Подивитися в OHIF у режимі Total Metabolic Tumor Volume",
|
||||||
|
"view_in_ohif_vr": "Подивитися в OHIF у режимі Volume Rendering",
|
||||||
|
"view_in_osimis": "Подивитись у OsimisViewer",
|
||||||
|
"view_in_stone": "Подивитись у StoneViewer",
|
||||||
|
"view_in_volview": "Подивитись у VolView",
|
||||||
|
"view_in_wsi_viewer": "Подивитись у Whole Slide Imaging Viewer"
|
||||||
|
}
|
||||||
208
WebApplication/src/locales/zh.json
Normal file
208
WebApplication/src/locales/zh.json
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
{
|
||||||
|
"all_modalities": "全部",
|
||||||
|
"anonymize": "匿名化",
|
||||||
|
"cancel": "取消",
|
||||||
|
"change_password": "更改密码",
|
||||||
|
"close": "关闭",
|
||||||
|
"copy_orthanc_id": "复制Orthanc ID",
|
||||||
|
"date_picker": {
|
||||||
|
"last_12_months": "最近12个月",
|
||||||
|
"last_month": "上个月",
|
||||||
|
"last_week": "上周",
|
||||||
|
"this_month": "本月",
|
||||||
|
"this_week": "本周",
|
||||||
|
"today": "今天",
|
||||||
|
"yesterday": "昨天"
|
||||||
|
},
|
||||||
|
"default": "默认",
|
||||||
|
"delete": "删除",
|
||||||
|
"delete_instance_body": "您确定要删除此实例吗?<br/>此操作无法撤销!",
|
||||||
|
"delete_instance_title": "删除实例?",
|
||||||
|
"delete_series_body": "您确定要删除此系列吗?<br/>此操作无法撤销!",
|
||||||
|
"delete_series_title": "删除系列?",
|
||||||
|
"delete_studies_body": "您确定要删除这些研究吗?<br/>此操作无法撤销!",
|
||||||
|
"delete_studies_title": "删除研究?",
|
||||||
|
"delete_study_body": "您确定要删除此研究吗?<br/>此操作无法撤销!",
|
||||||
|
"delete_study_title": "删除研究?",
|
||||||
|
"description": "描述",
|
||||||
|
"dicom_modalities": "DICOM模态",
|
||||||
|
"dicom_tags": {
|
||||||
|
"AccessionNumber": "访问号",
|
||||||
|
"BodyPartExamined": "检查的部位",
|
||||||
|
"InstanceNumber": "实例号",
|
||||||
|
"InstitutionName": "机构名称",
|
||||||
|
"ModalitiesInStudy": "研究中的模态",
|
||||||
|
"Modality": "模态",
|
||||||
|
"NumberOfFrames": "帧数",
|
||||||
|
"NumberOfStudyRelatedSeries": "系列数量",
|
||||||
|
"PatientBirthDate": "患者出生日期",
|
||||||
|
"PatientID": "患者ID",
|
||||||
|
"PatientName": "患者姓名",
|
||||||
|
"PatientSex": "患者性别",
|
||||||
|
"ProtocolName": "协议名称",
|
||||||
|
"ReferringPhysicianName": "转诊医师姓名",
|
||||||
|
"RequestingPhysician": "申请医师",
|
||||||
|
"SeriesDate": "系列日期",
|
||||||
|
"SeriesDescription": "系列描述",
|
||||||
|
"SeriesInstanceUID": "系列实例UID",
|
||||||
|
"SeriesNumber": "系列号",
|
||||||
|
"SeriesTime": "系列时间",
|
||||||
|
"StudyDate": "研究日期",
|
||||||
|
"StudyDescription": "研究描述",
|
||||||
|
"StudyID": "研究ID",
|
||||||
|
"StudyInstanceUID": "研究实例UID",
|
||||||
|
"StudyTime": "研究时间",
|
||||||
|
"seriesCount": "系列数量"
|
||||||
|
},
|
||||||
|
"dicom_web_servers": "DICOM-Web服务器",
|
||||||
|
"displaying_latest_studies": "显示最近的研究",
|
||||||
|
"download_dicom_file": "下载DICOM文件",
|
||||||
|
"download_dicomdir": "下载DICOMDIR",
|
||||||
|
"download_zip": "下载ZIP",
|
||||||
|
"drop_files": "将文件拖放到此处或",
|
||||||
|
"enter_search": "输入搜索条件以显示结果!",
|
||||||
|
"error": "错误",
|
||||||
|
"file": "文件",
|
||||||
|
"files": "文件",
|
||||||
|
"frames": "帧数",
|
||||||
|
"instance": "实例",
|
||||||
|
"instances": "实例",
|
||||||
|
"instances_number": "实例数量",
|
||||||
|
"labels": {
|
||||||
|
"add_button": "添加",
|
||||||
|
"add_labels_message_html": "<strong>添加</strong>这些标签到所有选定的资源",
|
||||||
|
"add_labels_placeholder": "要添加的标签,按Enter键创建新标签",
|
||||||
|
"added_labels_message_part_1_html": "已添加标签",
|
||||||
|
"added_labels_message_part_2_html": "到<strong>{count}</strong>个研究",
|
||||||
|
"clear_all_button": "移除全部",
|
||||||
|
"clear_all_labels_message_html": "<strong>移除全部</strong>选定资源的标签",
|
||||||
|
"cleared_labels_message_part_1_html": "已移除标签",
|
||||||
|
"cleared_labels_message_part_2_html": "从<strong>{count}</strong>个研究",
|
||||||
|
"edit_labels_button": "编辑标签",
|
||||||
|
"modal_title": "修改选定资源的标签",
|
||||||
|
"remove_button": "移除",
|
||||||
|
"remove_labels_message_html": "<strong>移除</strong>所有选定资源的这些标签",
|
||||||
|
"remove_labels_placeholder": "要移除的标签",
|
||||||
|
"removed_labels_message_part_1_html": "已移除标签",
|
||||||
|
"removed_labels_message_part_2_html": "从<strong>{count}</strong>个研究"
|
||||||
|
},
|
||||||
|
"legacy_ui": "传统用户界面",
|
||||||
|
"loading_latest_studies": "正在加载最近的研究",
|
||||||
|
"local_studies": "所有本地研究",
|
||||||
|
"logout": "登出",
|
||||||
|
"mammography_plugin": "Start mammogram AI",
|
||||||
|
"modalities_in_study": "研究中的模态",
|
||||||
|
"modify": {
|
||||||
|
"anonymize_button_title": "匿名化",
|
||||||
|
"anonymize_modal_title": "匿名化",
|
||||||
|
"autogenerated_dicom_uid": "自动生成",
|
||||||
|
"back_button_title": "返回",
|
||||||
|
"error_attach_series_to_existing_study_target_does_not_exist_html": "更改系列时出错,未找到具有此StudyInstanceUID的研究。",
|
||||||
|
"error_attach_series_to_existing_study_target_unchanged_html": "更改系列时出错,目标研究与当前系列研究相同。",
|
||||||
|
"error_attach_study_to_existing_patient_target_does_not_exist_html": "更改患者时出错,未找到具有此PatientID的患者。您应该使用<strong>修改研究标签</strong>功能。",
|
||||||
|
"error_modify_any_study_tags_can_not_modify_patient_tags_because_of_other_studies_html": "修改研究标签时出错:该患者有其他研究。您应该使用<strong>修改患者标签</strong>功能。",
|
||||||
|
"error_modify_any_study_tags_patient_exists_html": "修改研究标签时出错:已存在具有相同PatientID的其他患者。您应该使用<strong>更改患者</strong>功能。",
|
||||||
|
"error_modify_patient_tags_another_patient_exists_with_same_patient_id_html": "修改患者标签时出错:已存在具有相同PatientID的其他患者。您应该使用<strong>修改患者标签</strong>功能。",
|
||||||
|
"error_modify_unexpected_error_html": "修改过程中发生意外错误,请检查Orthanc日志",
|
||||||
|
"insert_tag": "插入DICOM标签",
|
||||||
|
"job_in_progress": "正在处理修改",
|
||||||
|
"modify_button_title": "修改",
|
||||||
|
"modify_modal_title": "修改",
|
||||||
|
"modify_study_mode_duplicate_html": "创建原始研究的修改副本。<small>(生成新的DICOM UID并保留原始研究)</small>",
|
||||||
|
"modify_study_mode_keep_uids_html": "修改原始研究。<small>(保留原始DICOM UID)</small>",
|
||||||
|
"modify_study_mode_new_uids_html": "修改原始研究。<small>(生成新的DICOM UID)</small>",
|
||||||
|
"remove_tag_tooltip": "移除标签",
|
||||||
|
"remove_tag_undo_tooltip": "撤销移除标签",
|
||||||
|
"series_step_0_attach_series_to_existing_study_button_title_html": "更改研究",
|
||||||
|
"series_step_0_attach_series_to_existing_study_html": "将此系列移至现有研究。",
|
||||||
|
"series_step_0_create_new_study_button_title_html": "创建新研究",
|
||||||
|
"series_step_0_create_new_study_html": "将此系列移至新的研究。",
|
||||||
|
"series_step_0_modify_series_button_title_html": "修改系列标签",
|
||||||
|
"series_step_0_modify_series_html": "仅修改此系列的系列标签?",
|
||||||
|
"show_modified_resources": "显示修改后的资源",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_button_title_html": "更改患者",
|
||||||
|
"study_step_0_attach_study_to_existing_patient_html": "将此研究附加到其他现有患者。",
|
||||||
|
"study_step_0_modify_study_button_title_html": "修改研究标签",
|
||||||
|
"study_step_0_modify_study_html": "仅修改此研究的患者/研究标签?",
|
||||||
|
"study_step_0_patient_has_other_studies_button_title_html": "修改患者标签",
|
||||||
|
"study_step_0_patient_has_other_studies_html": "该患者共有<strong>{count}</strong>个研究。<br/>您是否要在所有这些研究中修改患者?",
|
||||||
|
"warning_create_new_study_from_series_html": "已存在具有不同标签的具有相同PatientID的患者。您是否要使用现有的PatientName、PatientSex和PatientBirthDate?"
|
||||||
|
},
|
||||||
|
"my_jobs": "我的作业",
|
||||||
|
"no_modalities": "无",
|
||||||
|
"no_result_found": "未找到结果!",
|
||||||
|
"not_showing_all_results": "未显示所有结果。您应该精确您的搜索条件",
|
||||||
|
"open": "打开",
|
||||||
|
"page_not_found": "页面未找到!",
|
||||||
|
"plugin": "插件",
|
||||||
|
"preview": "预览",
|
||||||
|
"profile": "配置文件",
|
||||||
|
"retrieve_and_view": {
|
||||||
|
"finding_locally": "检查本地是否已存在该研究。",
|
||||||
|
"finding_remotely": "在远程DICOM节点中搜索该研究。",
|
||||||
|
"not_found": "未在本地或远程DICOM节点中找到该研究。",
|
||||||
|
"retrieved_html": "已检索<strong>{count}</strong>个实例。",
|
||||||
|
"retrieving": "检索研究。"
|
||||||
|
},
|
||||||
|
"searching": "搜索中...",
|
||||||
|
"select_files": "选择文件",
|
||||||
|
"select_folder": "选择文件夹",
|
||||||
|
"send_to": {
|
||||||
|
"button_title": "发送",
|
||||||
|
"dicom": "发送至DICOM节点",
|
||||||
|
"dicom_web": "发送至DICOMWeb服务器",
|
||||||
|
"orthanc_peer": "发送至Orthanc对等节点",
|
||||||
|
"transfers": "发送至Orthanc对等节点(高级传输)"
|
||||||
|
},
|
||||||
|
"series": "系列",
|
||||||
|
"series_count_header": "系列数",
|
||||||
|
"series_plural": "系列",
|
||||||
|
"settings": {
|
||||||
|
"dicom_AET": "DICOM AET",
|
||||||
|
"dicom_port": "DICOM端口",
|
||||||
|
"ingest_transcoding": "摄取转码",
|
||||||
|
"installed_plugins": "已安装的插件",
|
||||||
|
"orthanc_name": "Orthanc名称",
|
||||||
|
"orthanc_system_info": "Orthanc系统信息",
|
||||||
|
"orthanc_version": "Orthanc版本",
|
||||||
|
"overwrite_instances": "覆盖实例",
|
||||||
|
"plugins_not_enabled": "已加载但未启用或配置不正确的插件为",
|
||||||
|
"statistics": "统计",
|
||||||
|
"storage_compression": "存储压缩",
|
||||||
|
"storage_size": "存储大小",
|
||||||
|
"striked_through": "删除线",
|
||||||
|
"title": "设置",
|
||||||
|
"verbosity_level": "详细级别"
|
||||||
|
},
|
||||||
|
"share": {
|
||||||
|
"anonymize": "匿名化?",
|
||||||
|
"button_title": "共享",
|
||||||
|
"copy_and_close": "复制并关闭",
|
||||||
|
"days": "天",
|
||||||
|
"expires_in": "过期时间",
|
||||||
|
"link": "链接",
|
||||||
|
"modal_title": "共享研究",
|
||||||
|
"never": "永不"
|
||||||
|
},
|
||||||
|
"show_errors": "显示错误",
|
||||||
|
"studies": "研究",
|
||||||
|
"study": "研究",
|
||||||
|
"this_patient_has_no_other_studies": "该患者没有其他研究。",
|
||||||
|
"this_patient_has_other_studies": "该患者共有{count}个研究。",
|
||||||
|
"this_patient_has_other_studies_show": "显示它们!",
|
||||||
|
"token": {
|
||||||
|
"error_token_expired_html": "您的令牌已过期。",
|
||||||
|
"error_token_invalid_html": "您的令牌无效。<br/>请确保完整粘贴或与提供给您的人员联系。",
|
||||||
|
"error_token_unknown_html": "您的令牌无效。<br/>请确保完整粘贴或与提供给您的人员联系。",
|
||||||
|
"token_being_checked_html": "正在验证您的令牌。"
|
||||||
|
},
|
||||||
|
"trace": "跟踪",
|
||||||
|
"upload": "上传",
|
||||||
|
"uploaded_studies": "已上传的研究",
|
||||||
|
"verbose": "详细",
|
||||||
|
"view_in_meddream": "在MedDream中查看",
|
||||||
|
"view_in_ohif": "在OHIF中查看",
|
||||||
|
"view_in_osimis": "在OsimisViewer中查看",
|
||||||
|
"view_in_stone": "在StoneViewer中查看",
|
||||||
|
"view_in_volview": "在VolView中查看"
|
||||||
|
}
|
||||||
41
WebApplication/src/main-landing.js
Normal file
41
WebApplication/src/main-landing.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import AppLanding from './AppLanding.vue'
|
||||||
|
import i18n from './locales/i18n'
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css"
|
||||||
|
import "bootstrap/dist/js/bootstrap.bundle.min.js"
|
||||||
|
import "bootstrap-icons/font/bootstrap-icons.css"
|
||||||
|
import "@fortawesome/fontawesome-free/css/all.min.css"
|
||||||
|
import store from "./store"
|
||||||
|
import orthancApi from './orthancApi'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
|
||||||
|
// Names of the params that can contain an authorization token
|
||||||
|
// If one of these params contain a token, it will be passed as a header
|
||||||
|
// with each request to the Orthanc API
|
||||||
|
const VALID_TOKEN_PARAMS = ["token", "auth-token", "authorization"];
|
||||||
|
|
||||||
|
|
||||||
|
// before initialization, we must load part of the configuration to know if we need to enable Keycloak or not
|
||||||
|
axios.get('../api/pre-login-configuration').then((config) => {
|
||||||
|
|
||||||
|
const app = createApp(AppLanding)
|
||||||
|
|
||||||
|
app.use(store)
|
||||||
|
app.use(i18n)
|
||||||
|
|
||||||
|
// If there is a param with a token in the params, use it as a header in subsequent calls to the Orthanc API
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
for (let paramName of VALID_TOKEN_PARAMS) {
|
||||||
|
const paramValue = params.get(paramName);
|
||||||
|
|
||||||
|
if (!paramValue) continue;
|
||||||
|
|
||||||
|
localStorage.setItem(paramName, paramValue);
|
||||||
|
orthancApi.updateAuthHeader(paramName);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.mount('#app-landing')
|
||||||
|
|
||||||
|
});
|
||||||
41
WebApplication/src/main-retrieve-and-view.js
Normal file
41
WebApplication/src/main-retrieve-and-view.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import AppRetrieveAndView from './AppRetrieveAndView.vue'
|
||||||
|
import i18n from './locales/i18n'
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css"
|
||||||
|
import "bootstrap/dist/js/bootstrap.bundle.min.js"
|
||||||
|
import "bootstrap-icons/font/bootstrap-icons.css"
|
||||||
|
import "@fortawesome/fontawesome-free/css/all.min.css"
|
||||||
|
import store from "./store"
|
||||||
|
import orthancApi from './orthancApi'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
|
||||||
|
// Names of the params that can contain an authorization token
|
||||||
|
// If one of these params contain a token, it will be passed as a header
|
||||||
|
// with each request to the Orthanc API
|
||||||
|
const VALID_TOKEN_PARAMS = ["token", "auth-token", "authorization"];
|
||||||
|
|
||||||
|
|
||||||
|
// before initialization, we must load part of the configuration to know if we need to enable Keycloak or not
|
||||||
|
axios.get('../api/pre-login-configuration').then((config) => {
|
||||||
|
|
||||||
|
const app = createApp(AppRetrieveAndView)
|
||||||
|
|
||||||
|
app.use(store)
|
||||||
|
app.use(i18n)
|
||||||
|
|
||||||
|
// If there is a param with a token in the params, use it as a header in subsequent calls to the Orthanc API
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
for (let paramName of VALID_TOKEN_PARAMS) {
|
||||||
|
const paramValue = params.get(paramName);
|
||||||
|
|
||||||
|
if (!paramValue) continue;
|
||||||
|
|
||||||
|
localStorage.setItem(paramName, paramValue);
|
||||||
|
orthancApi.updateAuthHeader(paramName);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.mount('#app-retrieve-and-view')
|
||||||
|
|
||||||
|
});
|
||||||
115
WebApplication/src/main.js
Normal file
115
WebApplication/src/main.js
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import i18n from './locales/i18n'
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css"
|
||||||
|
import "bootstrap/dist/js/bootstrap.bundle.min.js"
|
||||||
|
import "bootstrap-icons/font/bootstrap-icons.css"
|
||||||
|
import "@fortawesome/fontawesome-free/css/all.min.css"
|
||||||
|
import "./assets/css/layout.css"
|
||||||
|
import "./assets/css/common.css"
|
||||||
|
import store from "./store"
|
||||||
|
import { router } from './router'
|
||||||
|
import Keycloak from "keycloak-js"
|
||||||
|
import orthancApi from './orthancApi'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Datepicker from '@vuepic/vue-datepicker';
|
||||||
|
import '@vuepic/vue-datepicker/dist/main.css';
|
||||||
|
import mitt from "mitt"
|
||||||
|
import VueObserveVisibility from 'vue3-observe-visibility'
|
||||||
|
|
||||||
|
// Names of the params that can contain an authorization token
|
||||||
|
// If one of these params contain a token, it will be passed as a header
|
||||||
|
// with each request to the Orthanc API
|
||||||
|
const VALID_TOKEN_PARAMS = ["token", "auth-token", "authorization"];
|
||||||
|
|
||||||
|
// before initialization, we must load part of the configuration to know if we need to enable Keycloak or not
|
||||||
|
axios.get('../api/pre-login-configuration').then((config) => {
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
const messageBus = mitt();
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
app.use(store)
|
||||||
|
app.use(i18n)
|
||||||
|
app.use(VueObserveVisibility)
|
||||||
|
app.component('Datepicker', Datepicker);
|
||||||
|
|
||||||
|
app.config.globalProperties.messageBus = messageBus;
|
||||||
|
|
||||||
|
let keycloackConfig = null;
|
||||||
|
|
||||||
|
if (config.data['Keycloak'] && config.data['Keycloak'] != null && config.data['Keycloak']['Enable']) {
|
||||||
|
console.log("Keycloak is enabled");
|
||||||
|
|
||||||
|
keycloackConfig = config.data['Keycloak']
|
||||||
|
|
||||||
|
let initOptions = {
|
||||||
|
url: keycloackConfig['Url'],
|
||||||
|
realm: keycloackConfig['Realm'],
|
||||||
|
clientId: keycloackConfig['ClientId'],
|
||||||
|
onLoad: 'login-required'
|
||||||
|
}
|
||||||
|
|
||||||
|
window.keycloak = new Keycloak(initOptions);
|
||||||
|
|
||||||
|
window.keycloak.init({ onLoad: initOptions.onLoad }).then(async (auth) => {
|
||||||
|
|
||||||
|
if (!auth) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
console.log("Authenticated");
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem("vue-token", window.keycloak.token);
|
||||||
|
localStorage.setItem("vue-refresh-token", window.keycloak.refreshToken);
|
||||||
|
orthancApi.updateAuthHeader();
|
||||||
|
|
||||||
|
app.mount('#app');
|
||||||
|
console.log("App mounted with keycloak, current route is ", router.currentRoute.value.fullPath);
|
||||||
|
|
||||||
|
// keycloak includes state, code and session_state -> the router does not like them -> remove them
|
||||||
|
const params = new URLSearchParams(router.currentRoute.value.fullPath);
|
||||||
|
params.delete('state');
|
||||||
|
params.delete('code');
|
||||||
|
params.delete('session_state');
|
||||||
|
const cleanedRoute = decodeURIComponent(params.toString()).replace('/=', '/');
|
||||||
|
console.log("App mounted, moving to cleaned route ", cleanedRoute);
|
||||||
|
await router.push(cleanedRoute);
|
||||||
|
|
||||||
|
// programm token refresh at regular interval
|
||||||
|
setInterval(() => {
|
||||||
|
window.keycloak.updateToken(70).then((refreshed) => {
|
||||||
|
if (refreshed) {
|
||||||
|
console.log('Token refreshed');
|
||||||
|
localStorage.setItem("vue-token", window.keycloak.token);
|
||||||
|
localStorage.setItem("vue-refresh-token", window.keycloak.refreshToken);
|
||||||
|
orthancApi.updateAuthHeader();
|
||||||
|
} else {
|
||||||
|
console.log('Token not refreshed, valid for '
|
||||||
|
+ Math.round(window.keycloak.tokenParsed.exp + window.keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
console.log('Failed to refresh token');
|
||||||
|
});
|
||||||
|
|
||||||
|
}, 60000)
|
||||||
|
|
||||||
|
}).catch(() => {
|
||||||
|
console.log("Could not connect to Keycloak");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If there is a param with a token in the params, use it as a header in subsequent calls to the Orthanc API
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
for (let paramName of VALID_TOKEN_PARAMS) {
|
||||||
|
const paramValue = params.get(paramName);
|
||||||
|
|
||||||
|
if (!paramValue) continue;
|
||||||
|
|
||||||
|
localStorage.setItem(paramName, paramValue);
|
||||||
|
orthancApi.updateAuthHeader(paramName);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
}
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user