Added mammography plugin button on instance level

This commit is contained in:
Aljaž Gerečnik 2025-02-15 10:13:20 +01:00
commit 3e63ff2a37
119 changed files with 34543 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
WebApplication/node_modules
WebApplication/dist/
ThirdPartyDownloads/
.DS_Store
dist-ssr
*.local
.vscode/
*~
build

27
AUTHORS Normal file
View 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
View 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
View 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
View 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/>.

View 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
View 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
View 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
View 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.

View 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

View 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/*

View 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

View 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)

View 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()

View 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()

View 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()

View 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()

View 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()

View 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()

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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
}

View 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()

View File

@ -0,0 +1,12 @@
# This is a version-script for Orthanc plugins
{
global:
OrthancPluginInitialize;
OrthancPluginFinalize;
OrthancPluginGetName;
OrthancPluginGetVersion;
local:
*;
};

View 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.

File diff suppressed because it is too large Load Diff

View 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()

View 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)

View 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)

View 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)

View 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
View 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 its 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
View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View 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>

View 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>

View 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>

View 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>

View 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;
}

View 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;
} */

View 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;
} */

View File

@ -0,0 +1,3 @@
.content {
margin-left: var(--nav-bar-width);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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">&nbsp;</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>

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -0,0 +1,3 @@
export var baseOe2Url = window.location.pathname.slice(0, -1);
export var orthancApiUrl = '../../'
export var oe2ApiUrl = '../api/';

View 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)
}
}
},
}

View 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;
}
}

View 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, '');
}
}

View 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;
}
}
}

View File

@ -0,0 +1,7 @@
const SourceType = Object.freeze({
LOCAL_ORTHANC: 0,
REMOTE_DICOM: 1,
REMOTE_DICOM_WEB: 2
});
export default SourceType;

View 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"
}

View 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"
}

View 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"
}

View 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"
}

View 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
};

View 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"
}

View 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-ში ნახვა"
}

View 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"
}

View 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"
}

View 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"
}

View 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中查看"
}

View 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')
});

View 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
View 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