diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..5de13d239d46bfab8be30d649d470b3bc0100bd2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Exclude all binaries from the diff +*.exe binary +*.dll binary +*.so binary +*.a binary +*.png binary +*.ico binary + +# The unciado linux installer +./UNICADOworkflow/UNICADOinstaller binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..59bd72deae04f30e96290d7da84b768e060aa726 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# Project specific +*.layout +*.res +*.depend +*.layout +*.cscope_file_list +*.log +test_runner_config.json + +# IDE Files +.vscode +*.cbp + +# Prerequisites +*.d + +# Compiled object files +*.slo +*.lo +*.o +*.obj + +# precompiled headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled static libraries +*.lai +*.la +*.a +*.lib + +# Executable files +*.exe +*.out +*.app + +# Build directories +build +package + +# Package artifacts +*.zip +*.ZIP +_CPack_Packages +UNICADOinstaller.spec diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.gitlab/issue_templates/Default.md b/.gitlab/issue_templates/Default.md new file mode 100644 index 0000000000000000000000000000000000000000..ed6f8d8cfb4c948a3cb438fb92d89159e2d1a352 --- /dev/null +++ b/.gitlab/issue_templates/Default.md @@ -0,0 +1,18 @@ +<!-- Title: Provide a concise and descriptive title for the issue incl. tool or library name --> + +## Choose Your Issue Template +Before creating an issue, please review existing issues to avoid duplicates! +ALso, select the **appropriate type for your issue in the desciption drop-down**. +- Bug Report Template +- Feature Request Template +- TODO Template +- Documentation Request Template +- Testing Request Template + +If not suitable, use the sections below and contact the owners. + +## Description +Provide a concise description of the issue. + +## Additional Context +Add screenshots, logs, or relevant information here. diff --git a/.gitlab/issue_templates/Todo.md b/.gitlab/issue_templates/Todo.md new file mode 100644 index 0000000000000000000000000000000000000000..07bdd0f8b2e7294d7949344698e4426530efc826 --- /dev/null +++ b/.gitlab/issue_templates/Todo.md @@ -0,0 +1,16 @@ +<!-- Title: Provide a concise and descriptive title for the issue incl. tool or library name --> +# TODO + +## Summary +Briefly describe the task. Provide any background information or related issues. + +## Subtasks +- [ ] Step 1 +- [ ] Step 2 +- [ ] Step 3 + +## Acceptance Criteria +- [ ] Define measurable outcomes for success. +- [ ] List specific requirements. + +/label ~"type::todo" diff --git a/.gitlab/issue_templates/bug_report.md b/.gitlab/issue_templates/bug_report.md new file mode 100644 index 0000000000000000000000000000000000000000..b906703dce8a5a8d1ca99bd5a36d980f6a2fc26a --- /dev/null +++ b/.gitlab/issue_templates/bug_report.md @@ -0,0 +1,22 @@ +<!-- Title: Provide a concise and descriptive title for the issue incl. tool or library name --> +# Bug Report + +## Description +Describe the bug clearly. What happened? + +## Steps to Reproduce +1. [Step 1] +2. [Step 2] +3. [Step 3] + +## Expected Behavior +Explain what you expected to see. + +## Environment +- **OS**: [e.g., Windows 10] +- **Version/Branch**: [e.g., v1.2.3] + +## Additional Context +Attach any logs, screenshots, or context. + +/label ~"type::bug" diff --git a/.gitlab/issue_templates/documentation_request.md b/.gitlab/issue_templates/documentation_request.md new file mode 100644 index 0000000000000000000000000000000000000000..bc94cf6d704aa0645ae950ba690cc716b81659f1 --- /dev/null +++ b/.gitlab/issue_templates/documentation_request.md @@ -0,0 +1,14 @@ +<!-- Title: Provide a concise and descriptive title for the issue --> +# Documentation + +## Summary +Explain what documentation is missing. + +- **Unicado Version**: vx.x.x +- **Page**: page-to-change + +## Additional Context +Attach any logs, screenshots, or context. + +/label ~"type::documentation" + diff --git a/.gitlab/issue_templates/feature_request.md b/.gitlab/issue_templates/feature_request.md new file mode 100644 index 0000000000000000000000000000000000000000..2c12fdb6bfc6f26eec57a4c7b1b228b65d991e24 --- /dev/null +++ b/.gitlab/issue_templates/feature_request.md @@ -0,0 +1,17 @@ +<!-- Title: Provide a concise and descriptive title for the issue incl. tool or library name --> +# Feature Request + +## Summary +What feature are you requesting? + +## Why? +Explain the problem this feature solves or the value it adds. + +## Acceptance Criteria +- [ ] Define measurable outcomes for success. +- [ ] List specific requirements. + +## Additional Notes +Include references, examples, or diagrams if applicable. + +/label ~"type::feature" diff --git a/.gitlab/issue_templates/testing_request.md b/.gitlab/issue_templates/testing_request.md new file mode 100644 index 0000000000000000000000000000000000000000..0289f4df34cec1c786d637480d7456bbf20b3d08 --- /dev/null +++ b/.gitlab/issue_templates/testing_request.md @@ -0,0 +1,24 @@ +<!-- Title: Provide a concise and descriptive title for the issue incl. tool or library name --> +# Testing Issue + +## Summary +Provide a brief overview of what should be tested or changed in the test process. Also think about what is the goal of this test? E.g. +- Verify that [feature/bug fix/module] works as expected. +- Ensure that [specific requirement] is met. + +## Related Issues or Merge Requests +- Issue(s): #[Issue ID] +- Merge Request(s): #[Merge Request ID] + +## Expected Results +- [Outcome 1]: [What should happen]. +- [Outcome 2]: [Another expected result]. + +## Environment +- **OS**: [e.g., Windows 10] +- **Version/Branch**: [e.g., v1.2.3] + +## Additional Context +Attach any logs, screenshots, or context. + +/label ~"type::testing" diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md new file mode 100644 index 0000000000000000000000000000000000000000..00be17dc34a7fc90bb7096112e57c895a2f6d4bf --- /dev/null +++ b/.gitlab/merge_request_templates/Default.md @@ -0,0 +1,29 @@ +<!-- Title: Use an imperative, clear title (e.g., "Fix login bug" or "Add new analytics feature") --> + +## Description +Provide a concise explanation of the changes made in this merge request. + +## Related Issue(s) +- Closes #[feature issues] +- Fixes #[bug report issue] +- Resolves #[any other issues] + +### Other Changes +- [Mention refactoring, tests, etc.] + +## Screenshots/Logs +Attach screenshots or log outputs if applicable. + +## Testing Instructions +1. [Step 1: How to test] +2. [Step 2: Expected outcome] +3. [Step 3: Additional steps if required] + +## Developer Checklist +- [ ] Code has been tested locally and/or in pipeline. +- [ ] (if applicable) documentation updated. +- [ ] (if applicable) impact of new dependencies reviewed and included in project. +- [ ] Merge conflicts resolved with the target branch. + +## Additional Notes +Add any information reviewers should focus on, e.g., specific files, functions, or changes of interest. diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000000000000000000000000000000000..6b220a10092cab8a0a8cec28ff669d03861048ac --- /dev/null +++ b/.gitmodules @@ -0,0 +1,21 @@ +[submodule "aircraft_design"] + path = aircraft_design + url = ssh://git@git.rwth-aachen.de/unicado/aircraft-design.git +[submodule "utilities"] + path = utilities + url = ssh://git@git.rwth-aachen.de/unicado/utilities.git +[submodule "aircraft_references"] + path = aircraft_references + url = ssh://git@git.rwth-aachen.de/unicado/aircraft-references.git +[submodule "libraries"] + path = libraries + url = ssh://git@git.rwth-aachen.de/unicado/libraries.git +[submodule "engines"] + path = engines + url = ssh://git@git.rwth-aachen.de/unicado/engines.git +[submodule "rce-workflow"] + path = rce-workflow + url = ssh://git@git.rwth-aachen.de/unicado/rce-workflow.git +[submodule "rce_workflow"] + path = rce_workflow + url = ssh://git@git.rwth-aachen.de/unicado/rce-workflow.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..dd6e9516af94a575dddaf6489a632fc961396980 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,85 @@ +# Set a recent minimum version +cmake_minimum_required(VERSION 3.29) + +# Start the project +project(UNICADO + VERSION 3.0.0 + DESCRIPTION "Calculate all the things!" + LANGUAGES CXX +) + +# Set the C++ standard +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Check whether parallel build is enabled +if(NOT DEFINED ENV{CMAKE_BUILD_PARALLEL_LEVEL}) + # Give the user a warning, since almost all PCs have multiple cores nowadays + message(WARNING "\n[${PROJECT_NAME}] -> Parallel build is not enabled. Consider setting the environment variable CMAKE_BUILD_PARALLEL_LEVEL to the number of available cores.") +endif() + +# Disable the min and max macros when building with MSVC +if(MSVC) + add_compile_definitions(NOMINMAX) +endif() + +# *optional* Add cppcheck and cpplint +# set(CMAKE_CXX_CPPCHECK "cppcheck") +# set(CMAKE_CXX_CPPLINT "cpplint") + +# Option for blackbox testing +option(BUILD_BLACKBOXTESTS "Use blackbox testing" OFF) +# Option for unit testing +option(BUILD_UNITTEST "Use unit testing" OFF) +# Option to include system libraries in the package archive +option(PACKAGE_SYSTEM_LIBRARIES "Include system libraries in the package archive" OFF) +# Finally decide whether to build static or shared libraries +option(BUILD_SHARED_LIBS "Build shared libraries" OFF) +# Option where to look for the libraries +option(FIND_LIBRARIES_AS_PACKAGE "If true the libraries are included with find_package(), otherwise the submodule is used." ON) +# Option to link glibs statically +option(STATIC_GLIBS "Link glibs statically" OFF) +# Option to compile Python modules to an executable +option(BUILD_PYTHON_MODULES "If true the python modules will be compiled to executables." OFF) + +# Link system libs statically if static glibs is enabled +if(STATIC_GLIBS) + message(DEPRECATION "Linking with static glibs is enabled. This is not guaranted to work and will be removed due to the native compiler support!") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static") +endif() + +# Get the path to the c/c++ runtime libraries to include in the package if enabled +if(PACKAGE_SYSTEM_LIBRARIES) + get_filename_component( SYSTEM_LIBS_PATH ${CMAKE_CXX_COMPILER} PATH ) + message(STATUS "[${PROJECT_NAME}] -> Packaging System Libraries from: ${SYSTEM_LIBS_PATH}") + message(DEPRECATION "Packaging system libraries is enabled. This will be removed in the future due to the native compiler support!") +endif() + +# Setup the python environment +find_program(PIPENV pipenv) +if(PIPENV) + message(STATUS "[${PROJECT_NAME}] -> Python/pipenv found. Updating required packages from Pipfile...") + execute_process( + COMMAND ${PIPENV} update + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} # Execute in the top level source directory + OUTPUT_QUIET + ) +else() + message(WARNING "[${PROJECT_NAME}] -> pipenv not found. Python support is disabled!") +endif() + +# *** Important *** +# Set the path to the libraries package manually +set(UnicadoLibs_DIR ${CMAKE_CURRENT_LIST_DIR}/libraries/cmake) + +# Add the library and tell the other packages to not look for them +add_subdirectory(libraries) +set(UnicadoLibs_FOUND TRUE) + +# Add the software components +add_subdirectory(aircraft_design) +add_subdirectory(utilities) + +# Include the package script +include(cmake/PackageUnicado.cmake) +add_subdirectory(installer) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000000000000000000000000000000000000..0396d3d39b7b3d01fbe014b0f12e62a357e6408b --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,216 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "unix-common", + "description": "Common settings for Unix compilers", + "hidden": true, + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_CXX_FLAGS": "-fexceptions -fno-builtin" + } + + }, + { + "name": "unix-debug", + "description": "Base settings for building debug configuration with Unix compilers.", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS_DEBUG": "-Wextra -Wsign-conversion -Wfloat-equal -g" + } + }, + { + "name": "unix-release", + "description": "Base settings for building release configuration with Unix compilers.", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_CXX_FLAGS_RELEASE": "-O2 -s" + } + }, + { + "name": "windows-common", + "description": "Common settings for Windows compilers", + "hidden": true, + "binaryDir": "${sourceDir}/build", + "generator": "Visual Studio 17 2022", + "toolset": "ClangCL", + "toolchainFile": "C:/dev/vcpkg/scripts/buildsystems/vcpkg.cmake", + "cacheVariables": { + "Python_EXECUTABLE": {"type": "FILEPATH", "value": "$ENV{HOMEDRIVE}$ENV{HOMEPATH}/AppData/Local/Programs/Python/Python311/python.exe"}, + "CMAKE_CXX_FLAGS": "/permissive- /EHsc" + } + }, + { + "name": "windows-debug", + "description": "Base settings for building debug configuration with Windows compilers.", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_FLAGS_DEBUG": "/W4" + } + }, + { + "name": "windows-release", + "description": "Base settings for building release configuration with Windows compilers.", + "hidden": true, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_CXX_FLAGS_RELEASE": "/O2" + } + }, + { + "name": "x64-linux-debug", + "description": "Default debug configuration for building on Linux", + "generator": "Unix Makefiles", + "inherits": ["unix-common", "unix-debug"] + }, + { + "name": "x64-linux-release", + "description": "Default release configuration for building on Linux", + "generator": "Unix Makefiles", + "inherits": ["unix-common", "unix-release"] + }, + { + "name": "x64-mingw-debug", + "description": "Default debug configuration for building with MSYS2/MinGW on Windows", + "generator": "MinGW Makefiles", + "inherits": ["unix-common", "unix-debug"] + }, + { + "name": "x64-mingw-release", + "description": "Default release configuration for building with MSYS2/MinGW on Windows", + "generator": "MinGW Makefiles", + "inherits": ["unix-common", "unix-release"] + }, + { + "name": "x64-windows-debug", + "description": "Default debug configuration for building on Windows", + "architecture": "x64", + "inherits": ["windows-common", "windows-debug"] + }, + { + "name": "x64-windows-release", + "description": "Default release configuration for building on Windows", + "architecture": "x64", + "inherits": ["windows-common", "windows-release"] + } + ], + "buildPresets": [ + { + "name": "x64-linux-debug", + "description": "Sets the build type to Debug for the Linux build system.", + "configurePreset": "x64-linux-debug" + }, + { + "name": "x64-linux-release", + "description": "Sets the build type to Release for the Linux build system.", + "configurePreset": "x64-linux-release" + }, + { + "name": "x64-linux-installer", + "inherits": "x64-linux-release", + "targets": "installer" + }, + { + "name": "linux-python-packages", + "inherits": "x64-linux-release", + "targets": "install_python_packages" + }, + { + "name": "x64-windows-debug", + "description": "Sets the build type to Debug for the Windows build system.", + "configurePreset": "x64-windows-debug", + "configuration": "Debug" + }, + { + "name": "x64-windows-release", + "description": "Sets the build type to Release for the Windows build system.", + "configurePreset": "x64-windows-release", + "configuration": "Release" + }, + { + "name": "x64-windows-installer", + "inherits": "x64-windows-release", + "targets": "installer" + }, + { + "name": "windows-python-packages", + "inherits": "x64-windows-release", + "targets": "install_python_packages" + } + ], + "packagePresets": [ + { + "name": "x64-linux-release", + "description": "Create the package file of the Linux release version.", + "configurePreset": "x64-linux-release", + "configFile": "${sourceDir}/build/CPackConfig.cmake", + "generators": ["ZIP"], + "packageName": "UNICADO", + "packageDirectory": "${sourceDir}/installer" + }, + { + "name": "x64-windows-release", + "description": "Create the package file of the Windows release version.", + "configurePreset": "x64-windows-release", + "configFile": "${sourceDir}/build/CPackConfig.cmake", + "generators": ["ZIP"], + "packageName": "UNICADO", + "packageDirectory": "${sourceDir}/installer" + } + ], + "workflowPresets": [ + { + "name": "x64-linux-release", + "steps": [ + { + "type": "configure", + "name": "x64-linux-release" + }, + { + "type": "build", + "name": "linux-python-packages" + }, + { + "type": "build", + "name": "x64-linux-release" + }, + { + "type": "package", + "name": "x64-linux-release" + }, + { + "type": "build", + "name": "x64-linux-installer" + } + ] + }, + { + "name": "x64-windows-release", + "steps": [ + { + "type": "configure", + "name": "x64-windows-release" + }, + { + "type": "build", + "name": "windows-python-packages" + }, + { + "type": "build", + "name": "x64-windows-release" + }, + { + "type": "package", + "name": "x64-windows-release" + }, + { + "type": "build", + "name": "x64-windows-installer" + } + ] + } + ] +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000000000000000000000000000000000..bae5ee300cdb91cbf76cbf47064a301a95edf9b8 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,13 @@ +# Repository-specific code ownership +* @Florian.Schueltke + +# File-specific code ownership +.gitattributes @Florian.Schueltke +.gitignore @Florian.Schueltke +.gitlab-ci.yml @maurice.zimmnau @kristina.mazur +.gitmodules @Florian.Schueltke +CMakeLists.txt @Florian.Schueltke +CMakePresets.json @Florian.Schueltke +CODEOWNERS @Florian.Schueltke +LICENSE @Florian.Schueltke +README.md @Florian.Schueltke diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..e06ae69696acc3a0209f8fb93f7e4203fd960279 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is 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. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + 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. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + 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 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. Use with the GNU Affero General Public License. + + 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 Affero 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 special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 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 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 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) 2025 UNICADO consortium + + 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 <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) 2025 UNICADO consortium + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + 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 GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000000000000000000000000000000000000..845615c4586e99b4d8350eedf9a431e238f54a85 --- /dev/null +++ b/Pipfile @@ -0,0 +1,21 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pyinstaller = "*" +pyqt5 = "*" +numpy = "*" +ambiance = "*" +matplotlib = "*" +yattag = "*" +termcolor = "*" +bs4 = "*" +pandas = "*" +openpyxl = "*" + +[dev-packages] + +[requires] +python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000000000000000000000000000000000000..caf89f47c231aaefa3bc84f413300fb0c6972895 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,763 @@ +{ + "_meta": { + "hash": { + "sha256": "ea76d0d42b38be487b49162bfd491011106d08810411dc447579d518b0ebe475" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "altgraph": { + "hashes": [ + "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", + "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff" + ], + "version": "==0.17.4" + }, + "ambiance": { + "hashes": [ + "sha256:ceff180945a96996da5a3aceff2f2ff4f1ae67dadd24919675223c8e01d0a416", + "sha256:d7ccd04390e59727ffca5c54079586fe0b40419db5445b524db563a9405f015a" + ], + "index": "pypi", + "markers": "python_full_version >= '3.6.0'", + "version": "==1.3.1" + }, + "beautifulsoup4": { + "hashes": [ + "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051", + "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed" + ], + "markers": "python_full_version >= '3.6.0'", + "version": "==4.12.3" + }, + "bs4": { + "hashes": [ + "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925", + "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc" + ], + "index": "pypi", + "version": "==0.0.2" + }, + "contourpy": { + "hashes": [ + "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1", + "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda", + "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d", + "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509", + "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6", + "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f", + "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e", + "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751", + "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86", + "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b", + "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc", + "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546", + "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec", + "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f", + "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82", + "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c", + "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b", + "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c", + "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c", + "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53", + "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80", + "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242", + "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85", + "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124", + "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5", + "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2", + "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3", + "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d", + "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc", + "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342", + "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1", + "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1", + "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595", + "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30", + "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab", + "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3", + "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2", + "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd", + "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7", + "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277", + "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453", + "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697", + "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b", + "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454", + "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9", + "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1", + "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6", + "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291", + "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750", + "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", + "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e", + "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81", + "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9", + "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375" + ], + "markers": "python_version >= '3.10'", + "version": "==1.3.1" + }, + "cycler": { + "hashes": [ + "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", + "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c" + ], + "markers": "python_version >= '3.8'", + "version": "==0.12.1" + }, + "et-xmlfile": { + "hashes": [ + "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", + "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54" + ], + "markers": "python_version >= '3.8'", + "version": "==2.0.0" + }, + "fonttools": { + "hashes": [ + "sha256:00f7cf55ad58a57ba421b6a40945b85ac7cc73094fb4949c41171d3619a3a47e", + "sha256:01124f2ca6c29fad4132d930da69158d3f49b2350e4a779e1efbe0e82bd63f6c", + "sha256:12db5888cd4dd3fcc9f0ee60c6edd3c7e1fd44b7dd0f31381ea03df68f8a153f", + "sha256:161d1ac54c73d82a3cded44202d0218ab007fde8cf194a23d3dd83f7177a2f03", + "sha256:1f0e115281a32ff532118aa851ef497a1b7cda617f4621c1cdf81ace3e36fb0c", + "sha256:23bbbb49bec613a32ed1b43df0f2b172313cee690c2509f1af8fdedcf0a17438", + "sha256:2863555ba90b573e4201feaf87a7e71ca3b97c05aa4d63548a4b69ea16c9e998", + "sha256:2b3ab90ec0f7b76c983950ac601b58949f47aca14c3f21eed858b38d7ec42b05", + "sha256:31d00f9852a6051dac23294a4cf2df80ced85d1d173a61ba90a3d8f5abc63c60", + "sha256:33b52a9cfe4e658e21b1f669f7309b4067910321757fec53802ca8f6eae96a5a", + "sha256:37dbb3fdc2ef7302d3199fb12468481cbebaee849e4b04bc55b77c24e3c49189", + "sha256:3e569711464f777a5d4ef522e781dc33f8095ab5efd7548958b36079a9f2f88c", + "sha256:3f901cef813f7c318b77d1c5c14cf7403bae5cb977cede023e22ba4316f0a8f6", + "sha256:51c029d4c0608a21a3d3d169dfc3fb776fde38f00b35ca11fdab63ba10a16f61", + "sha256:5435e5f1eb893c35c2bc2b9cd3c9596b0fcb0a59e7a14121562986dd4c47b8dd", + "sha256:553bd4f8cc327f310c20158e345e8174c8eed49937fb047a8bda51daf2c353c8", + "sha256:55718e8071be35dff098976bc249fc243b58efa263768c611be17fe55975d40a", + "sha256:61dc0a13451143c5e987dec5254d9d428f3c2789a549a7cf4f815b63b310c1cc", + "sha256:636caaeefe586d7c84b5ee0734c1a5ab2dae619dc21c5cf336f304ddb8f6001b", + "sha256:6c99b5205844f48a05cb58d4a8110a44d3038c67ed1d79eb733c4953c628b0f6", + "sha256:7208856f61770895e79732e1dcbe49d77bd5783adf73ae35f87fcc267df9db81", + "sha256:732a9a63d6ea4a81b1b25a1f2e5e143761b40c2e1b79bb2b68e4893f45139a40", + "sha256:7636acc6ab733572d5e7eec922b254ead611f1cdad17be3f0be7418e8bfaca71", + "sha256:7dd91ac3fcb4c491bb4763b820bcab6c41c784111c24172616f02f4bc227c17d", + "sha256:8118dc571921dc9e4b288d9cb423ceaf886d195a2e5329cc427df82bba872cd9", + "sha256:81ffd58d2691f11f7c8438796e9f21c374828805d33e83ff4b76e4635633674c", + "sha256:838d2d8870f84fc785528a692e724f2379d5abd3fc9dad4d32f91cf99b41e4a7", + "sha256:8c9679fc0dd7e8a5351d321d8d29a498255e69387590a86b596a45659a39eb0d", + "sha256:9ce4ba6981e10f7e0ccff6348e9775ce25ffadbee70c9fd1a3737e3e9f5fa74f", + "sha256:a656652e1f5d55b9728937a7e7d509b73d23109cddd4e89ee4f49bde03b736c6", + "sha256:a7ad1f1b98ab6cb927ab924a38a8649f1ffd7525c75fe5b594f5dab17af70e18", + "sha256:aa046f6a63bb2ad521004b2769095d4c9480c02c1efa7d7796b37826508980b6", + "sha256:abe62987c37630dca69a104266277216de1023cf570c1643bb3a19a9509e7a1b", + "sha256:b2e526b325a903868c62155a6a7e24df53f6ce4c5c3160214d8fe1be2c41b478", + "sha256:b5263d8e7ef3c0ae87fbce7f3ec2f546dc898d44a337e95695af2cd5ea21a967", + "sha256:b7ef9068a1297714e6fefe5932c33b058aa1d45a2b8be32a4c6dee602ae22b5c", + "sha256:bca35b4e411362feab28e576ea10f11268b1aeed883b9f22ed05675b1e06ac69", + "sha256:ca7fd6987c68414fece41c96836e945e1f320cda56fc96ffdc16e54a44ec57a2", + "sha256:d12081729280c39d001edd0f4f06d696014c26e6e9a0a55488fabc37c28945e4", + "sha256:dd2820a8b632f3307ebb0bf57948511c2208e34a4939cf978333bc0a3f11f838", + "sha256:e198e494ca6e11f254bac37a680473a311a88cd40e58f9cc4dc4911dfb686ec6", + "sha256:e7e6a352ff9e46e8ef8a3b1fe2c4478f8a553e1b5a479f2e899f9dc5f2055880", + "sha256:e8e67974326af6a8879dc2a4ec63ab2910a1c1a9680ccd63e4a690950fceddbe", + "sha256:f0a4b52238e7b54f998d6a56b46a2c56b59c74d4f8a6747fb9d4042190f37cd3", + "sha256:f27526042efd6f67bfb0cc2f1610fa20364396f8b1fc5edb9f45bb815fb090b2", + "sha256:f307f6b5bf9e86891213b293e538d292cd1677e06d9faaa4bf9c086ad5f132f6", + "sha256:f46b863d74bab7bb0d395f3b68d3f52a03444964e67ce5c43ce43a75efce9246", + "sha256:f50a1f455902208486fbca47ce33054208a4e437b38da49d6721ce2fef732fcf", + "sha256:f8c8c76037d05652510ae45be1cd8fb5dd2fd9afec92a25374ac82255993d57c", + "sha256:fa34aa175c91477485c44ddfbb51827d470011e558dfd5c7309eb31bef19ec51" + ], + "markers": "python_version >= '3.8'", + "version": "==4.55.0" + }, + "kiwisolver": { + "hashes": [ + "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", + "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95", + "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", + "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", + "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d", + "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", + "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", + "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", + "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", + "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", + "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", + "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", + "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", + "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", + "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", + "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", + "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", + "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", + "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", + "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e", + "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", + "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", + "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", + "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", + "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", + "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", + "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", + "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5", + "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", + "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", + "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", + "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", + "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", + "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", + "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", + "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", + "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3", + "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a", + "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", + "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", + "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", + "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", + "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", + "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", + "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", + "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", + "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", + "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", + "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", + "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", + "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b", + "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", + "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", + "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", + "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", + "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", + "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", + "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", + "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", + "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", + "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", + "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", + "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", + "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933", + "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", + "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", + "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", + "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503", + "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", + "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", + "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", + "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", + "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", + "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", + "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf", + "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d", + "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", + "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", + "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", + "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", + "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2", + "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", + "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade", + "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a", + "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c", + "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", + "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00", + "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", + "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", + "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", + "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", + "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", + "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09", + "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", + "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", + "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89", + "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", + "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", + "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", + "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", + "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", + "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", + "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d", + "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935", + "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", + "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", + "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b", + "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", + "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", + "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", + "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", + "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", + "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", + "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.7" + }, + "matplotlib": { + "hashes": [ + "sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21", + "sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5", + "sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697", + "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9", + "sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca", + "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64", + "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e", + "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03", + "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae", + "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa", + "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3", + "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e", + "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a", + "sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc", + "sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea", + "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b", + "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e", + "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447", + "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b", + "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92", + "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb", + "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66", + "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9", + "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7", + "sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2", + "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30", + "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d", + "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7", + "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4", + "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41", + "sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2", + "sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556", + "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f", + "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772", + "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c", + "sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a", + "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51", + "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49", + "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c", + "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==3.9.2" + }, + "numpy": { + "hashes": [ + "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe", + "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", + "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48", + "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", + "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", + "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", + "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17", + "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", + "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", + "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", + "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", + "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4", + "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6", + "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4", + "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", + "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f", + "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f", + "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f", + "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56", + "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", + "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd", + "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23", + "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed", + "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", + "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098", + "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1", + "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", + "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f", + "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", + "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f", + "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", + "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", + "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0", + "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", + "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef", + "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5", + "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e", + "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", + "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d", + "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43", + "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c", + "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", + "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff", + "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408", + "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", + "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", + "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57", + "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb", + "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", + "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3", + "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a", + "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0", + "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", + "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598", + "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==2.1.3" + }, + "openpyxl": { + "hashes": [ + "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", + "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.1.5" + }, + "packaging": { + "hashes": [ + "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", + "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f" + ], + "markers": "python_version >= '3.8'", + "version": "==24.2" + }, + "pandas": { + "hashes": [ + "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", + "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", + "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", + "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", + "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", + "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", + "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea", + "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", + "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", + "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", + "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", + "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", + "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", + "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e", + "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", + "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", + "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", + "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30", + "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", + "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", + "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", + "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", + "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", + "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", + "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", + "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761", + "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", + "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", + "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c", + "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c", + "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", + "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", + "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", + "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", + "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", + "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39", + "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", + "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", + "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", + "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", + "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", + "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.2.3" + }, + "pefile": { + "hashes": [ + "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", + "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6" + ], + "markers": "sys_platform == 'win32'", + "version": "==2023.2.7" + }, + "pillow": { + "hashes": [ + "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7", + "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5", + "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903", + "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2", + "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38", + "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2", + "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9", + "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f", + "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc", + "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8", + "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d", + "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2", + "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316", + "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a", + "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25", + "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd", + "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba", + "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc", + "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273", + "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa", + "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a", + "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b", + "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a", + "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae", + "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291", + "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97", + "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06", + "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904", + "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b", + "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b", + "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8", + "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527", + "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947", + "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb", + "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003", + "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5", + "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f", + "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739", + "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944", + "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830", + "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f", + "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3", + "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4", + "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84", + "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7", + "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6", + "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6", + "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9", + "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de", + "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4", + "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47", + "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd", + "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50", + "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c", + "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086", + "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba", + "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306", + "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699", + "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e", + "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488", + "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa", + "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2", + "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3", + "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9", + "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923", + "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2", + "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790", + "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734", + "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916", + "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1", + "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f", + "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798", + "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb", + "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2", + "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9" + ], + "markers": "python_version >= '3.9'", + "version": "==11.0.0" + }, + "pyinstaller": { + "hashes": [ + "sha256:0d6475559c4939f0735122989611d7f739ed3bf02f666ce31022928f7a7e4fda", + "sha256:208c0ef6dab0837a0a273ea32d1a3619a208e3d1fe3fec3785eea71a77fd00ce", + "sha256:2e8365276c5131c9bef98e358fbc305e4022db8bedc9df479629d6414021956a", + "sha256:32c742a24fe65d0702958fadf4040f76de85859c26bec0008766e5dbabc5b68f", + "sha256:35e6b8077d240600bb309ed68bb0b1453fd2b7ab740b66d000db7abae6244423", + "sha256:44e36172de326af6d4e7663b12f71dbd34e2e3e02233e181e457394423daaf03", + "sha256:491dfb4d9d5d1d9650d9507daec1ff6829527a254d8e396badd60a0affcb72ef", + "sha256:6d12c45a29add78039066a53fb05967afaa09a672426072b13816fe7676abfc4", + "sha256:7ac83c0dc0e04357dab98c487e74ad2adb30e7eb186b58157a8faf46f1fa796f", + "sha256:ad84abf465bcda363c1d54eafa76745d77b6a8a713778348377dc98d12a452f7", + "sha256:ddc0fddd75f07f7e423da1f0822e389a42af011f9589e0269b87e0d89aa48c1f", + "sha256:e21c7806e34f40181e7606926a14579f848bfb1dc52cbca7eea66eccccbfe977" + ], + "index": "pypi", + "markers": "python_version < '3.14' and python_version >= '3.8'", + "version": "==6.11.1" + }, + "pyinstaller-hooks-contrib": { + "hashes": [ + "sha256:8a46655e5c5b0186b5e527399118a9b342f10513eb1425c483fa4f6d02e8800c", + "sha256:ad47db0e153683b4151e10d231cb91f2d93c85079e78d76d9e0f57ac6c8a5e10" + ], + "markers": "python_version >= '3.8'", + "version": "==2024.10" + }, + "pyparsing": { + "hashes": [ + "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84", + "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c" + ], + "markers": "python_version >= '3.9'", + "version": "==3.2.0" + }, + "pyqt5": { + "hashes": [ + "sha256:6cd75628f6e732b1ffcfe709ab833a0716c0445d7aec8046a48d5843352becb6", + "sha256:76be0322ceda5deecd1708a8d628e698089a1cea80d1a49d242a6d579a40babd", + "sha256:bdde598a3bb95022131a5c9ea62e0a96bd6fb28932cc1619fd7ba211531b7517", + "sha256:c8b03dd9380bb13c804f0bdb0f4956067f281785b5e12303d529f0462f9afdc2", + "sha256:cd672a6738d1ae33ef7d9efa8e6cb0a1525ecf53ec86da80a9e1b6ec38c8d0f1", + "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==5.15.11" + }, + "pyqt5-qt5": { + "hashes": [ + "sha256:1988f364ec8caf87a6ee5d5a3a5210d57539988bf8e84714c7d60972692e2f4a", + "sha256:750b78e4dba6bdf1607febedc08738e318ea09e9b10aea9ff0d73073f11f6962", + "sha256:76980cd3d7ae87e3c7a33bfebfaee84448fd650bad6840471d6cae199b56e154", + "sha256:9cc7a768b1921f4b982ebc00a318ccb38578e44e45316c7a4a850e953e1dd327" + ], + "version": "==5.15.2" + }, + "pyqt5-sip": { + "hashes": [ + "sha256:0b718a362f4392430903bbb2a4b9bbff9841a16a52f0cfdd5b5bbd9d11457980", + "sha256:0c1c727ede7fdc464a1fe2e46109ba836509b2d7187a46fdeae443148ce51d1c", + "sha256:0cd21c3215e3c47fdd5fa7a2dc3dd1e07a7230b0626e905a7217925068c788b9", + "sha256:13f0c6a78e781255863e3e160304648efaf62276b7102741af637b63a6e96930", + "sha256:24a1d4937332bf0a38dd95bb2ce4d89723df449f6e912b52ef0e107e11fefac1", + "sha256:2575f428de584a12009fd29d00c89df16ed101a3b38beba818dfdcbc4a10709c", + "sha256:749f7a3ffd6e3d2d5db65ed92c95cbd14490631595c61f0c0672c9238bfb17de", + "sha256:7f88c85702dce80ac2e1a162054f688ed394811d6dd03a5574b3fa8111b0a6db", + "sha256:83d247cdc43ef224410b14c97413067ea26356dfa39e9ed0fe702a31e25710b0", + "sha256:852b75cf208825602480e95ab63314108f872d0da251e9ad3deaaff5a183a6f5", + "sha256:855563d4d3b59ce7438bbf2dd32fed2707787defa40f3efe94f204a19ef92b25", + "sha256:91b9538458a3a23e033c213bc879ce64f3d0a33d5a49cbd03e1e584efe307a35", + "sha256:97f2d6e8d9b7b3d3e795d576d7f56e6257f524221f6383b33ded7287763e9f06", + "sha256:b4adc529fa4ec05728e14ea55194d907cc51f18d6f2ac5cc9f6eb52ac038aa0f", + "sha256:b58eeedc9b2a3037b136bf96915196c391a33be470ed1c0723d7163ef0b727a2", + "sha256:c0c543d604116af26694a8a5ba90f510551ff9124d503ae5ee14bb73a61363a3", + "sha256:c85be433fbafcb3d417581c0e1b67c8198d23858166e4f938e971c2262c13cdb", + "sha256:d23fdfcf363b5cedd9d39f8a9c5710e7d52804f5b08a58e91c638b36eafcb702", + "sha256:dd241de9c569c07bbba62bff1049996e5b52478164f61f430073a87bf6d26d33", + "sha256:ed5221c6241981bd98d39504823efb9cbe36841bf8917288f8fe8fc1d5569a41", + "sha256:f600ae6f03e4bff91153c0dc7ebe52f90bd2b6afda58fd580e6990b3b951adc0" + ], + "markers": "python_version >= '3.8'", + "version": "==12.15.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==2.9.0.post0" + }, + "pytz": { + "hashes": [ + "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a", + "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725" + ], + "version": "==2024.2" + }, + "pywin32-ctypes": { + "hashes": [ + "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", + "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.2.3" + }, + "scipy": { + "hashes": [ + "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e", + "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79", + "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37", + "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5", + "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675", + "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d", + "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f", + "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310", + "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617", + "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e", + "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e", + "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417", + "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d", + "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94", + "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad", + "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8", + "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0", + "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69", + "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066", + "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3", + "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5", + "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07", + "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2", + "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389", + "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d", + "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84", + "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2", + "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3", + "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73", + "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06", + "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc", + "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1", + "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2" + ], + "markers": "python_version >= '3.10'", + "version": "==1.14.1" + }, + "setuptools": { + "hashes": [ + "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef", + "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829" + ], + "markers": "python_version >= '3.9'", + "version": "==75.5.0" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.16.0" + }, + "soupsieve": { + "hashes": [ + "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", + "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9" + ], + "markers": "python_version >= '3.8'", + "version": "==2.6" + }, + "termcolor": { + "hashes": [ + "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8", + "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==2.5.0" + }, + "tzdata": { + "hashes": [ + "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", + "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd" + ], + "markers": "python_version >= '2'", + "version": "==2024.2" + }, + "yattag": { + "hashes": [ + "sha256:baa8f254e7ea5d3e0618281ad2ff5610e0e5360b3608e695c29bfb3b29d051f4" + ], + "index": "pypi", + "version": "==1.16.1" + } + }, + "develop": {} +} diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..505df0f645bc7f5b54bfc4680c5db9479706f5fe --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# Welcome to the UNICADO package :airplane: +This repository is a container which collects all UNICADO repositories as submodules. It the best starting point for a newbie to get to know the overall tool chain. The repository is also used to create the standalone UNICADO installer to run the designs in RCE. + +> → Checkout your [UNICADO website](https://unicado.io/) for more information! + +## Installation + +### For User +You want to use UNICADO to get familiar with the workflow and see first results? Great :fire: Then check out the [Installation Guide](https://unicado.pages.rwth-aachen.de/unicado.gitlab.io/download/installation/) and the [Cleared for Take-Off](https://unicado.pages.rwth-aachen.de/unicado.gitlab.io/download/takeoff/). It includes the prerequisites, troubleshooting hints and a standalone installer. + +### For Developer +We welcome contributions! :sparkles: Please read our [developer guide](https://unicado.pages.rwth-aachen.de/unicado.gitlab.io/developer/developer-installation/). It explains step-by-step what you need to install, how to get the source code, how to build it, how to contribute and how to create the installer. + +## License +This project is licensed under the GNU General Public License, Version 3 License - see the [LICENSE](LICENSE) file for details. + +## Acknowledgments and Funding +This project is a collaborative effort by +- RWTH Aachen, Chair and Institute of Aerospace Systems +- TU Berlin + - Aircraft Design and Aerostructures + - Flight Mechanics, Flight Control and Aeroelasticity +- TU Braunschweig, Chair of Overall Aircraft Design +- TU Hamburg, Institute of Air Transportation Systems +- TU Munich, Chair of Aircraft Design +- University of Stuttgart, Institute of Aircraft Design + +including its associated partner +- Airbus SE +- Collins Aerospace +- TGM Lightweight Solutions GmbH + +and is funded by the Federal Ministry of Economic Affairs and Climate Action on the basis of a decision by the German Bundestag. + +## Contact +For questions or support, feel free to contact us :email: **E-Mail:** [contacts@unicado.io](mailto:contacts@unicado.io). + diff --git a/aircraft_design b/aircraft_design new file mode 160000 index 0000000000000000000000000000000000000000..f1c70dad7cffec6776c26d4521c328f824f6fe21 --- /dev/null +++ b/aircraft_design @@ -0,0 +1 @@ +Subproject commit f1c70dad7cffec6776c26d4521c328f824f6fe21 diff --git a/aircraft_references b/aircraft_references new file mode 160000 index 0000000000000000000000000000000000000000..23227daf5f425cc2536df5b75f7faba2774a7813 --- /dev/null +++ b/aircraft_references @@ -0,0 +1 @@ +Subproject commit 23227daf5f425cc2536df5b75f7faba2774a7813 diff --git a/cmake/PackageUnicado.cmake b/cmake/PackageUnicado.cmake new file mode 100644 index 0000000000000000000000000000000000000000..d0a72019adda7e84e907e28d60faf0e27f411ebc --- /dev/null +++ b/cmake/PackageUnicado.cmake @@ -0,0 +1,81 @@ +# Find and include all system runtime libraries if enabled +if(PACKAGE_SYSTEM_LIBRARIES) + # Get the path to the c/c++ runtime libraries to include in the package + get_filename_component( SYSTEM_LIBS_PATH ${CMAKE_CXX_COMPILER} PATH ) + message(STATUS "-> Packaging System Libraries from: ${SYSTEM_LIBS_PATH}") + message(DEPRECATION "Packaging system libraries is deprecated and will be removed in the future. Please use the native compiler for your OS where packaging the libraries should no longer be needed.") + + # Install the runtime dependencies + install( RUNTIME_DEPENDENCY_SET unicado_runtime_deps + PRE_INCLUDE_REGEXES "libgcc.*" "libstdc.*" "libwinpthread.*" + PRE_EXCLUDE_REGEXES ".*\.dll" ".*\.so" # Skip all other system libs for now + DIRECTORIES + ${SYSTEM_LIBS_PATH} + ${CMAKE_CURRENT_SOURCE_DIR}/libraries/unicadoRuntimeLibs + DESTINATION unicadoRuntimeLibs + ) +endif() + +# Include information about the project +install( + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/LICENSE + DESTINATION . +) + +# Install the include directory for report generation +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/utilities/report_generator/inc + DESTINATION report_generator/ +) + +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/aircraft_references/UNICADO-SMR + DESTINATION projects/ +) + +# Install airfoil data +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/libraries/airfoils/F15 + DESTINATION databases/airfoils/ +) +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/libraries/airfoils/NACA + DESTINATION databases/airfoils/ +) + +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/engines/PW1127G-JM + DESTINATION databases/engines/ +) +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/engines/V2527-A5 + DESTINATION databases/engines/ +) + +# Define the install rules for the worflow files +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/rce_workflow/jsonFiles + DESTINATION workflowComponent +) +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/rce_workflow/UNICADOworkflow + DESTINATION workflowComponent +) + +# Set the package information +set(CPACK_PACKAGE_NAME "UNICADO") + +# Use maximum available threads for packaging +set(CPACK_THREADS 0) + +# Set packaging options +set(CPACK_PACKAGE_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/installer) +if( WIN32 ) + set(CPACK_GENERATOR "ZIP") +elseif( UNIX ) + set(CPACK_GENERATOR "ZIP") +endif() +# set(CPACK_PACKAGE_CHECKSUM "SHA256") +# set(CPACK_PACKAGE_CHECKSUM_TYPE "SHA256") +include(CPack) diff --git a/engines b/engines new file mode 160000 index 0000000000000000000000000000000000000000..922a8f10da97099c74441349fbdaa0d210d9b262 --- /dev/null +++ b/engines @@ -0,0 +1 @@ +Subproject commit 922a8f10da97099c74441349fbdaa0d210d9b262 diff --git a/installer/CMakeLists.txt b/installer/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..72a46a62dd103981b7f77e39e962228af3e9e255 --- /dev/null +++ b/installer/CMakeLists.txt @@ -0,0 +1,49 @@ +# Set the target separators according to the OS +if(WIN32) + set(TARGET_SEPARATOR "\;") + set(TARGET_ID "win64") + set(TARGET_EXT "zip") + set(INSTALLER_EXECUTABLE "UNICADOinstaller.exe") +else() + set(TARGET_SEPARATOR ":") + set(TARGET_ID "Linux") + set(TARGET_EXT "zip") + set(INSTALLER_EXECUTABLE "UNICADOinstaller") +endif() + +add_custom_target(debug-info + COMMAND echo "${PROJECT_NAME}-${PROJECT_VERSION}-${TARGET_ID}.${TARGET_EXT}" +) + +# Set the installer commands +set(PYINSTALLER_OPTIONS + --noconfirm + --onefile + --windowed + --icon "${CMAKE_CURRENT_LIST_DIR}/iconUNICADO.ico" + --add-data "${CMAKE_CURRENT_LIST_DIR}/${PROJECT_NAME}-${PROJECT_VERSION}-${TARGET_ID}.${TARGET_EXT}${TARGET_SEPARATOR}." + --add-data "${CMAKE_CURRENT_LIST_DIR}/sub_functions${TARGET_SEPARATOR}sub_functions/" + --add-data "${CMAKE_CURRENT_LIST_DIR}/image_rc.py${TARGET_SEPARATOR}." + --add-data "${CMAKE_CURRENT_LIST_DIR}/unicadoICON.svg${TARGET_SEPARATOR}." + --add-data "${CMAKE_CURRENT_LIST_DIR}/UNICADOLogo90.svg${TARGET_SEPARATOR}." + --add-data "${CMAKE_CURRENT_LIST_DIR}/own_python_packages.txt${TARGET_SEPARATOR}." + --add-data "${CMAKE_CURRENT_LIST_DIR}/python_module_list.txt${TARGET_SEPARATOR}." + --add-data "${CMAKE_CURRENT_LIST_DIR}/standard_python_packages.txt${TARGET_SEPARATOR}." + --add-data "${CMAKE_CURRENT_LIST_DIR}/version.txt${TARGET_SEPARATOR}." +) +set(PYINSTALLER_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/UNICADOinstaller.py") + +# Add the pyinstaller target if pipenv is found +if(PIPENV) + add_custom_target(installer + COMMAND ${PIPENV} run pyinstaller ${PYINSTALLER_OPTIONS} ${PYINSTALLER_SCRIPT} + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/dist/${INSTALLER_EXECUTABLE} ${CMAKE_SOURCE_DIR} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Building workflow installer for ${PROJECT_NAME} with pyinstaller" + BYPRODUCTS + ${CMAKE_CURRENT_LIST_DIR}/dist + ${CMAKE_SOURCE_DIR}/${INSTALLER_EXECUTABLE} + ) +else() + message(WARNING "-> pipenv not found, installer target will not be available") +endif() diff --git a/installer/UNICADOLogo90.svg b/installer/UNICADOLogo90.svg new file mode 100644 index 0000000000000000000000000000000000000000..90767cb3448143cb678802aafd55dc021b5846d6 --- /dev/null +++ b/installer/UNICADOLogo90.svg @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="45mm" height="182mm" version="1.1" viewBox="0 0 45 182" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <clipPath id="clip0-82"> + <rect x="-176" y="-103" width="1625" height="837"/> + </clipPath> + <clipPath id="clip1-87"> + <rect x="726" y="168" width="241" height="235"/> + </clipPath> + <clipPath id="clip2-48"> + <rect x="726" y="168" width="241" height="235"/> + </clipPath> + <clipPath id="clip3-49"> + <rect x="726" y="168" width="241" height="235"/> + </clipPath> + <linearGradient id="fill4-5" x1="802.87" x2="700.23" y1="961.54" y2="1064.2" gradientUnits="userSpaceOnUse"> + <stop stop-color="#6C6C6C" offset="0"/> + <stop offset="1"/> + </linearGradient> + <linearGradient id="fill5" x1="759.5" x2="623.11" y1="1024.8" y2="1053" gradientUnits="userSpaceOnUse"> + <stop stop-color="#00427D" offset="0"/> + <stop stop-color="#007DEC" offset="1"/> + </linearGradient> + <linearGradient id="fill6" x1="655.12" x2="723.78" y1="927.65" y2="1019.8" gradientUnits="userSpaceOnUse"> + <stop stop-color="#E20E1F" offset="0"/> + <stop stop-color="#F24657" offset="1"/> + </linearGradient> + <linearGradient id="fill7-95" x1="851.63" x2="712.36" y1="960.46" y2="924.63" gradientUnits="userSpaceOnUse"> + <stop stop-color="#EA7D92" offset="0"/> + <stop stop-color="#A91B36" stop-opacity=".8902" offset="1"/> + </linearGradient> + <clipPath id="clip8-5"> + <rect x="789" y="168" width="106" height="115"/> + </clipPath> + <clipPath id="clip10-3"> + <rect x="788" y="167" width="107" height="117"/> + </clipPath> + <linearGradient id="fill11-7" x1="867.34" x2="787.59" y1="1016.6" y2="929.84" gradientUnits="userSpaceOnUse"> + <stop stop-color="#204B78" offset="0"/> + <stop stop-color="#6199D4" offset="1"/> + </linearGradient> + <clipPath id="clip12-3"> + <rect width="900647" height="903600"/> + </clipPath> + <clipPath id="clip14-7"> + <rect width="900647" height="900647"/> + </clipPath> + <clipPath id="clip15-61"> + <rect x="1216" y="205" width="197" height="198"/> + </clipPath> + <clipPath id="clip16-42"> + <rect x="1216" y="205" width="197" height="198"/> + </clipPath> + <clipPath id="clip17-4"> + <rect x="1216" y="205" width="197" height="198"/> + </clipPath> + </defs> + <metadata> + <rdf:RDF> + <cc:Work rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/> + <dc:title/> + </cc:Work> + </rdf:RDF> + </metadata> + <g transform="matrix(-.0042763 -.49238 .49238 -.0042763 2.5055 182.11)"> + <g transform="matrix(.26458 0 0 .26458 -11.633 -26.742)" clip-path="url(#clip0-82)"> + <g clip-path="url(#clip1-87)"> + <g clip-path="url(#clip2-48)"> + <g clip-path="url(#clip3-49)"> + <g stroke-width=".37715"> + <path transform="matrix(1 0 0 1.0002 99.788 -668.56)" d="m802.87 961.54s23.324 44.524-8.89 81.894c-40.158 46.59-93.749 20.74-93.749 20.74s43.37-9.91 71.659-40.67c20.654-22.46 30.98-61.964 30.98-61.964z" fill="url(#fill4-5)" stroke="#161616"/> + <path transform="matrix(1 0 0 1.0002 99.788 -668.56)" d="m759.5 1024.8s-67.962 59.57-122.57 18.05c-29.925-22.75 13.47-72.734 13.47-72.734s-22.776 34.064 15.086 53.614c27.449 14.17 94.018 1.07 94.018 1.07z" fill="url(#fill5)" stroke="#444"/> + <path transform="matrix(1 0 0 1.0002 99.788 -668.56)" d="m686.82 882.21s-11.775 28.955-1.905 74.673c7.495 34.712 38.86 62.864 38.86 62.864s-56.332-1.17-68.957-59.436c-10.515-48.527 32.002-78.101 32.002-78.101z" fill="url(#fill6)" stroke="#e30e1f" stroke-opacity=".98431"/> + <path transform="matrix(1 0 0 1.0002 99.788 -668.56)" d="m712.36 924.63s29.45-44.075 81.627-32.058c56.571 13.028 57.65 67.887 57.65 67.887s-42.896-35.083-68.965-41.217c-22.881-5.385-70.312 5.388-70.312 5.388z" fill="url(#fill7-95)" stroke="#c7314f" stroke-opacity=".95294"/> + </g> + <g clip-path="url(#clip8-5)"> + <g clip-path="url(#clip10-3)"> + <image x="788" y="167" width="107" height="117" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGsAAAB1CAYAAABJYx/nAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAxBSURBVHhe7Z1ZcxNXFsfzEeYbZCYLmUwSs3gBjDdsvAAGAgQDYQlJIDNJJsnAZOaJ8itU5c3FI1SqJsTj4EW2ZFmSpdZurZZtZIzsKDGjDBmXixcs5/3OOfJtaDct6UpuSS37/qtOaWv1cn86939u9237pa2kRp3jdxi7dY7fY9C3uUopBFKtc1yuGbL31AwJ5qpBwbNr0JbYOWBdxtjeP76ainup15GdAzbPjv7xvqohW0/1kKOTroarUNo74uis1Am3qoftnsohIQ6NTwAIqbi3Fu/cs6Ti7R/M5C2MPjP5U5+JvPlvjDHyx94x8kavEWIMl1neMWC1VA3Zu+nquTaqGp2zfo/ecROyKFA1JCQhe8hOjAErgcZOwRKBibDe+SE7sG3fr8Xrd0dJxQ+WBKxbx7vNPIVZtEfv9Nfo7MkqnUAgk1KBsFiASWGxAHsNAiAnYN29dBe4sgkzqVbvmqoZcSRrhh2kZthOqnV2IgcmwmIFJoWVCdhrdw2kot/yuHrY9g3dJS656iCTENKeEWcSguxOgRIjMzApLFZgclhyYK9+Z8DvClU6+yd0F7mwqturd/UAqCUIslfvJAhrPbA1WNUAiwXYOv/KAdhzWM+Bbes1JmFbt+jubl3t07uu1xlciX0GN6k1uNYiCzApLFZg+fiXCOuV7/Sp5/BDcdLd3lraOyC8vG/U46wbda9AEIxnwGSwsgErtH+JwF75lx7XZ9lSFWO93nW6btQTazB6SYPRQ+ohsgFDWKzACuVfIjBYR2LTD6rRm+oM7tuNxolk4xiCehFWNmDr/YsNmJr+JQJ7o9e0tGmB1UI5DnCC+00TpAljbIKkA4aw9mnYv0Rg274fna8ddeygh7g5VGtw1jcavZFms488g8UATAqLFVgx/esPEPD9xKYBVm90XQZASy0WP0FY64BRWACSCZgUllb8C4Ht6B9/VPZFBzR4d7PJt9o6HiAHAFYmYFJYrMC04l8iMPRkeujlpQaDpwcArSAoJVjZgElhsQIrpX+lgN2zmOnhl4fw19Uw6tG1WfxP261B0mZdg5Uuu1iBlYN/4ePOe9brtCm0LQQFje9st4YIghIjG7B1/sUITApLS/4Fn69qvqRHUC1mv77DFiIHIfBRCRYLMO5fBVbTmFd3SAiRQ0KYHITHTMBEWKzApLDKxb92DdhMtGm0pWaTvwcAPT1sDz+HxQhMCosVWLn4V+Wg9SptIm2oYczdDVCSnY5JctiOIQEmg8UCbDP5F3xnWTPjr/1j3kMAY+mII0IwWIBtNf/aNSToaHOVTg0Gb32rNRA76gRQGOtgsQPb7P716l3DStWg7QxttuJr/+jEy62WQPSYa4pgKAPj/iVm1/Z+a4w2XfHVbg3o33VNp0AxA5PBYgG2WfwLn5dksLzf5L35LsA57p4mUmDPYOUAbCv515t9puIWG81j/oqDQnjxhGcmBSstMO5fz2BJgRW12Ggx+8wnAZQUFhsw7fpX5aCQhPcSEJFUDNstu6FRq3W23spB250qOoc+FUPCI/CfvP0Llv2tKNmF3R9Aevqe9z6RAysr/xqxJ3cP22PwvgPiG5xQmsupIWxsgHoHMrIXIv4WgMvFv/B7dFWF0Vr3F4qdmogShJUNmAb9a3XviEufD5xs2mdwNCEA6DqfyLNLCVjBs6tt3D/cBaCksJiBldK/Rlyx2hGnDgHRQymoMOPA+x6nyy4RWMGyq2nM1w0gkl2+KJED06J/AajVfQZnX63e2V2qM9+7Bq06KEp+S9cdQre5qnp24cFCY8bP+GfJad8sYQFWKv+qN7qTKUhFyqJswkk0kEHOdMDw9iK6qDpqtfh7ugCSHFYuwIrhXzhhtG7UqcmbB/AuS6wc5cCgolxWLfNTWSWE42f9DwAWRvrsYgamsn/Vj3ridXp3ybo7VuGdlW/3mRelsPA5+Js6ZzVaLYEehPR+YI6wACumfzUZvfE6g/uG1iFJVaOz1cuBQQUZpR/nL2wEaMQ4gpLDKqV/IaxG44QFJ43SXS0rIbDt98ZjIjB8rNEJX9OP8xNmFUI6F1yDlSm7iuVfLWZfEiq+3nLKJiVh4QGV4qwIbMeA1UI/yl3YGIchq84HHxIMFmCF9q/9Jm8Mur3SXRNSWZhhb6W6RAOU8abVvMt4zKpzCCr0IixWYGr5F05pazR6nXVj/gq6e5tGeEESqsQkAtvZb71N32ZXKqvs4fjFcIxcCMUUgRXLvzoAFFR8znLv9jKpZtj+baoqHMzj4mSrxXcV4SAsVmCF8C/Mqv1Gr3kzgxJVOWDzvw4D55xvModGMn8wOa8IixXYRv0LM6vF5Lu5FUCh8DixpK/UCezzDJtNgXpo7KVLACsdsEL71yF76Clk1O2tAkrUbsiq7f3WOH2ZXZBVNz6MLBCExQpMTf/CzDpg8d2hu7PlVDkozDGPuY45pyIISwQmhcUKLF//OgKgmk0T/q2WUVJhOV8z7AjSl+nVNOY9dzH0cOWjyI8pWErACulfLRb/pq76WLVnxDVCn6ZXpzA5/PHUjwRhsQJTy786bMFZvBJNd2VLa2e/rSLjABl/0e95o4sISwQmhVVI/2q3BRN1Rjf/I42sarUGrn4EYC5Px1OwlIAVwr+g+ks2m3yX6W5wsajTPtl7BUAhLFZgG/UvzCwoKHroLnCx6oR7OnJl5iciApPCKpR/tVoCvKDIVTgQvhCeT36CsNIAk8JSw78OCqEY3thAd4GLVTAQ7vnz/Z8JwmIFthH/Ou6aSvKCIk8dd0/1ISwRmBSW2v6FXeABi5/7VD5Cz4BGXvpLdA2WEjAprI36V5s1yH0qX3VAyf7xVJwgLFZgUli5+NdheziBd0rSTXPlqsNCWPdpdJFgyGGp6V+nvNEVvORBN8uVj465p4KfzT5KwVICppZ/ddiCw3STXPkIvQMqtCcIixWYFBYrsCOOSKLZxLu/DakdxldXZuLkcwVYavkX/Bh496eG2sYD3X998B/y+YNHisCksPL1LxjD8e5PDR2xh3UIixVYrv510nt/iXd/Kum0b074Yu5FWGr4F3aB7eOhG3RTXBvVudDc4hdzCaIETAorH/866oxE+OBXJeFca2jw5JcPEVZ2YFJY2YCdDcwm28eD/BqVWhKLCzksVmCZ/AvnHdLNcKmhY87Ijb/FfiFfPfzlBWAb8a8TnpkneMmFboZLDR13TfciLBZgUliZgF2aXCDtFj6mUl3vB+cEOayN+tdRRyTKi4oC6D1vNHp1/r8pWOmyKxf/wsk27dYAz6pC6Hxg7sk1gJUNGKt/4RwOnlUFEE4g/CAcW5bDYgEmwvpU4l+XIvMrOJWNrp5LTXXYQmcQwrX5xxDK2ZWLfx1zT+V/PyxXZgGs7q8XfiV/X0BY6YGx+Nf50MPVRuMEn/xSKEHVdn09LDZgIiwpsE4hzAfAhdQpz/2er3/8lWTKrmzAENZZ/9xyC8+qwuqoM6L7B8LKAgxhfZXBv2A9/N+ZF1rvOqeew2IAJoUlArsQii112PgtOgXXWd+Dvn/G/0fk2cUKDL0KgPOJmsXQmcCcB2EpAZPCSgesyzcb4wPgIqlrYjYqwsoGTAoLA/3qoDXETysVSxfDsYAcFiuwk577cZ5VRdS54FxMCksEls2/0K867ZPcq4opJVhKwOT+dcbPvaroOh+MJZRgYaQDhsUFz6oSKBssJWAXQw8XeVaVQJlgYSj510nPNM+qUigbLAwpsMtT8aWC/isGLmVhV/ZhZGFZCZA8RGAnXNP8HGAphLAuTS4sKsGRB8L6cHLht3Ybn1pWMrF0g2Icd0/z61WlFCusz6KPki1mfr2qpEo3KJbHKU+U31tVarHAwvOAh22hTfM31MtWp/2zcSVA0ng/8CBCF+cqpRCEEiAxrsEguMMavEUX5yqlpBcfleKDyflZfmpJI+ryRS1KkMTA2U90Ua5SC6s8JUgYeHajw80nwmhGp32zd5RAYfByXWM66pzqUQL15VxipcMS0uT/TNyy6rSHFTML52bQRbi0IrwxQQ6Kl+saVZPZew7hSGHhmXhermtQeCER56pLYfGz6xoVZtAnMz+viqD42XWNS3qZ5EIoJtC3ubQoBISgeGFRBjrpmXEirCvTP/Hp0FrXcWfkNsLihUUZCAfGWFjwMxZlIBxr4a0/9CWXloU+1WYNfUtfcmldvLDQsl566f8TI2xy81b3IQAAAABJRU5ErkJggg=="/> + </g> + </g> + <path transform="matrix(1 0 0 1.0002 99.788 -668.56)" d="m787.6 929.84s80.712 25.689 79.434 85.154c-1.239 57.65-72.386 37.14-72.386 37.14s38.892 2.48 40.161-36.35c1.184-36.238-47.209-85.944-47.209-85.944z" fill="url(#fill11-7)" stroke="#3d6ea2" stroke-opacity=".98039" stroke-width=".37715"/> + </g> + </g> + </g> + <g transform="translate(63.407 399)" fill="#bfbfbf" font-family="Quantify, Quantify_MSFontService, Arial" font-size="398px" font-weight="400"> + <path d="m213.33-199v200.19h-54.526v-200.19zm-83.58 148.85 19.104 34.626v0.398c-13.797 12.205-30.911 18.308-51.342 18.308-32.371 0-55.322-8.756-68.854-26.268-12.471-15.655-18.706-40.596-18.706-74.824v-101.09h54.526v101.09c0 7.164 0.1327 14.063 0.398 20.696 0.5307 6.6333 1.99 12.471 4.378 17.512 2.6533 5.0413 6.6333 9.154 11.94 12.338 5.3067 2.9187 12.869 4.378 22.686 4.378 4.511 0 8.889-0.6633 13.134-1.99s8.225-3.0513 11.94-5.174z"/> + <path d="m159.18-199h54.128v199h-54.128zm183.88 23.084c6.633 8.225 11.409 18.573 14.328 31.044s4.378 27.86 4.378 46.168v98.704h-54.128v-98.704c0-7.429-0.265-14.461-0.796-21.094-0.265-6.899-1.592-12.869-3.98-17.91-2.388-5.307-6.235-9.419-11.542-12.338s-12.869-4.378-22.686-4.378c-9.287 0-17.91 1.99-25.87 5.97v0.398l-0.398-0.398-19.502-34.626 0.398-0.398c13.797-12.471 31.044-18.706 51.74-18.706 31.84 0 54.526 8.756 68.058 26.268z"/> + <path d="m383.66 0v-199h52.934v199zm0-278.6h52.934v46.566h-52.934z"/> + <path d="m605.74-76.018 42.984 28.258-0.398 0.398c-9.286 15.92-21.89 28.391-37.81 37.412-15.654 9.0213-32.636 13.532-50.944 13.532-14.328 0-27.727-2.6533-40.198-7.96-12.47-5.572-23.349-13.001-32.636-22.288-9.286-9.2867-16.716-20.165-22.288-32.636-5.306-12.471-7.96-25.737-7.96-39.8 0-14.063 2.654-27.329 7.96-39.8 5.572-12.471 13.002-23.349 22.288-32.636 9.287-9.287 20.166-16.583 32.636-21.89 12.471-5.572 25.87-8.358 40.198-8.358 18.308 0 35.29 4.511 50.944 13.532 15.92 9.021 28.524 21.492 37.81 37.412l0.398 0.398-43.382 28.258-0.398-0.398c-4.245-9.817-10.48-17.777-18.706-23.88-7.96-6.103-16.848-9.154-26.666-9.154-7.164 0-13.93 1.592-20.298 4.776-6.102 2.919-11.409 7.031-15.92 12.338-4.51 5.041-8.092 11.011-10.746 17.91-2.653 6.633-3.98 13.797-3.98 21.492 0 7.6947 1.327 14.859 3.98 21.492 2.654 6.6333 6.236 12.471 10.746 17.512 4.511 4.776 9.818 8.6233 15.92 11.542 6.368 2.6533 13.134 3.98 20.298 3.98 9.818 0 18.706-2.6533 26.666-7.96 8.226-5.572 14.461-13.267 18.706-23.084l0.398-0.796z"/> + <path d="m1134.2-278.6v278.6h-52.93l0.4-99.898c0-7.429-1.6-14.461-4.78-21.094-2.92-6.633-6.9-12.338-11.94-17.114-4.78-5.041-10.61-8.889-17.51-11.542-6.64-2.919-13.8-4.378-21.5-4.378-7.69 0-14.85 1.459-21.49 4.378-6.63 2.653-12.468 6.501-17.509 11.542-4.776 5.041-8.623 10.879-11.542 17.512s-4.378 13.797-4.378 21.492c0 7.6947 1.459 14.859 4.378 21.492s6.766 12.471 11.542 17.512c5.041 4.776 10.879 8.6233 17.509 11.542 6.64 2.9187 13.8 4.378 21.49 4.378 3.72 0 7.43-0.2653 11.15-0.796 3.71-0.5307 7.16-1.592 10.35-3.184h0.79l19.5 35.82-0.39 0.398c-13 10.083-28.92 15.124-47.76 15.124-14.33 0-27.73-2.6533-40.201-7.96-12.471-5.3067-23.349-12.603-32.636-21.89s-16.716-20.165-22.288-32.636c-5.307-12.471-7.96-25.737-7.96-39.8 0-14.063 2.653-27.329 7.96-39.8 5.572-12.471 13.001-23.349 22.288-32.636s20.165-16.583 32.636-21.89 25.871-7.96 40.201-7.96c27.06 0 47.76 10.083 62.09 30.248v-107.46z"/> + </g> + <g transform="matrix(.00013052 1.3718e-5 -1.3718e-5 .00013052 1234.4 133.92)" clip-path="url(#clip12-3)"> + <g transform="scale(1 1.0033)" clip-path="url(#clip14-7)"> + <image width="900647" height="900647" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJkAAACZCAYAAAA8XJi6AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAy2SURBVHhe7Z0LsFVVGccZe5dmPjLyiSn4osLKzKSxZmpytDCxpimLh5YwQmYDOVONghalpRNYOpHpZBZljkpkwL37APdxuDBF45RMGoVhJpFyDUtTC6Xfun4Htot1ztnv5/eb+c++95y91/q+b/3P2vvsc87eo5R6sHr16gvRzfKvoiRHs9ncb9WqVfevWbNmlxF//5vlXHlaUeKBmT6BHmgZzC/MdgfL8bKqooQHA830m8oldp9P9fX1vVk2UZRgeJ63P7PUJpepHLpVNlOUYDAznR/CYE8i3V0qwcEws30G6ioM+XHZVFE6Mzg4eACmWWibqIu2yeaK0hljMGakzQ4TtZU5hYFOkiYUpT0Y5lLbQEGEKW+RJhTFzdDQ0IGYZZFtniAys1ij0ThRmlKUvfE87yCM8heXgYKIWUw/VlLag0nm2qYJI8y5VZpSlL3BJF+0TRNWfX1950pzirIHzHEweshvloh6RJpUlD1w/HQhu7i/OgwTSrTxL5bHS7OK8gKY4kt+o8TUYmlWUUaNWrt27SGY4gbLJHH0BDPZcdK8UncajcYbMMXfLJPEEgf7H5bmlbrD8ddF6BGXUaKKGexhaV6pO5jrCpdJYmqH53njpAulrmCE0Rhsq2WORMRu8kbpRqkrGGEmu7O/2+ZISDsw2VjpSqkjmOBKyxSJCvOeJV0pdaO3t/dQTLDNNkXC2iLdKXWj2WweyvHXPxymSEzMYP9keax0qdQJBn6B3wxpCRN/R7pU6gIDfzizy022GdKQzmI1hAE3BnvMb4Q0xSx2vXRdHgh6Hm+D55klScyX5fvl6cpBbnMxhblGRGvg/ow+LU+Hgnau8bWTuujvQem6+GCkM9BqVyJtNF+M+EFpopSQx2IrL782M4hTZNWO9Pf3H8H6P7C2T12NRuMDEkKxoZDE604iqGhjnTEdf58pzRYec9KSeM0vqZ05tWRmC15MU2WzvfA870ieH3Ztm7IWSgjFhuIYY7gSiCUG5kpU6BODmOwsV+ztRD7mRxzTZPMRqN8lPJaHwYYx95skjGLjCD5xMRC/NrMc7CPdFoKwJvNpC4abzvI66/HMRE2/LWkUH1cCaYriXMXyQ9J9rsQwWd4a5hjwaEmj+DgSyEzGcAx0rl+sI45OB/6FFDV7n4RfDhjoVI7JQuq3FG6ShJQ59P89K57Cit30Jgm7PBD4e+1E8hSm/yo6R8LLDPo+lgG8zY6nYNqOxkjI5YJZJI1vZ8YSA34vy/nopRJmJmDwOfRtfkbmjCtnXSthlhOKG+ZEbNb62oYNG14moWYCfX7XiiFvbcf8R0l45aXgRjOz2wJm3ckSbqrQX6FMRt7vkdDKDwmZ0//ORIsizPZ7Xg/nSciJQtvHFHB3+YCEVx0YwMIbzQgz3McrfN7y5ctfIaHHgrznoCIej02UEKsFiTWsRAstzPb1OGajjaKexrhGQqwmJFgqoxkxE32D5Sslha5gzrHMiP6v+RRGxPWY+fBdQq0uJDvRTr4MCmI2BtFc96vrtzBy1NUSavUh2YkMiGcVoCy6emho6FWSym7IJ5OvRscRMb5bwq0PzA69rmKUQQzYNcuWLXv1wMDAOPJY4lqnoLp5cHCw+rtMPwzQ6Y5CqNJV9U5hBAGz9TiKoUpJlToZGwYSP43d0POuoqiSFS/qb0nZ6wkFmKdmS1e1ncn8UIh3YbQVdnFUyUhN5oNZ7VSd1RJXPQ/8u4HZfuUolqqNqNfZ6EbHc/U7hRGGRqPxToqkZusidoVXSclG4LHPoPnmB8PykNINingKRbunVVTVHjF7bTPXLpNSKXGhqO+gqM/Zha6zmK3eJuVRkoTiXq5mG/mo614piZIWFPpytNNf+LpIDZYxFLyOZpsg6StZwZuDkyn8UmsgKilyvULSVvKAQZiAKm02Zu63SrpKnjAYExiMu+0BKrvIydyFZLSkqRQB86rnnehd9mCVWeSzFc2QFJWi0Gg03sIA3WkPWNlFXov1ZGwBYRb4MgP0rD1gZRY5fR/DHSYpKkWAgRnPuzRzYeRKmY3Dg5vI62JJs1Ts2rVrH8Zj95XTzZKc5svT5YZEzD27n2kNVBWE2R5loGZJioUFY70EM5mrZzrzIIczZNXyQ0JP2AlWQZjN3HRitqRZGIjpTGJb54/VoVWyejUgoUqazCdz6ajPSbq5wKx0NjEEvVdUQzarDkzZH3MkWjkx0MPoEkk7dcz1RejP/IbjN654XGLd5xmP6n1dvC4m82mYnFMzG21PwlzmUg+uvjvpOd4lV/P3CDU02YiYNR7HDJ+XMsTCXM6BNq+2+wgjYumV5qpHXU3WEmYztyO8VMoRCradjDl+Z7cZVrTzP2myulCoqh/8B9EO9AUpSVt6enpeQ72+aW0bRztp73RpvrqoyV4s6rGE2eU4KY95B74vM745QbrRv15CmivdVBs1mVsY7Wcsr7UfT0rUfbkMQfVRk2Uvar6T2fE0GYJyQzJf4RX5X0luAf/vdfVrHv9oK3lV+jIH+p7nld9gJGK+T9Yyl53kfRs3bny5rKomy173SOnLDQb7uSO5F4l1Rq58zd9qsoxEzZfJEJUbM4uZKdmVpEsk/gvX46pkZfYs6FQZpnLDAeUUV5JFFfFez9Lcyc35fFXECz/XD+oTJexMlrc4CB5n4ubv2cRtvqLjXK/kWjoyOFWCabnrMVlB1JSQd0PsszDbo451y6onJbVqYWYzcwzgSLho2stkLZrN5qE8X7rbUFt6lsOBUySlaoLR/OfJCicGYKyE2pb+/v43ml8nubYvgWZKGtWHwSzkL5WCmMwPL5oZyPzQ19lekUScd0rY9YLEi3RXkba7ym5gzovY/hGrvSLpGWr9dgm3flCA8exC77CKkocim6wFbXyWwXRdMzZP1dtgfjDaSRQjz3ehx0oosenp6TmE47YbHH1kLlNTCUtpMTAwcCLFud0uVgZKzGQtyOX1DLL57tdDVl9Z6XYJRXFBgcy3Q12FS1zMooPSbWrQz3S0xd9vmiKnpzlWPFm6V1xQqMxMxmwzR7pNHfqbRn/moytnLEkJk02XLpV2UKgsZ7JjpNvMWLly5YH0vciOJQmRz0+lG6UTFOtWu3hpKQ+TtRgcHDyAGBbaMcURM+VUaV7pBIU6z1XAlLQIo10gXecCMSR179FhaVLpRsYmG5E5VvI87yAJIVN6enqOdsUUQWqyoORhspZ4V3aZhJEZ9Ps6ZtM/2bFEkJosKAz0CRT9aUcRUxf9PszyYAklM+hztj+OKOLFuUSaU4LAYJuf7TuLmbaazebuH9RmBf2OseMIK2o2RZpTgpCnyeh7nYSRKfQb687JarKQ5GmyPGYyA7u7Wa54gkpNFpK8TEa/uRyTGej7KDueoMKgetAfFgo+2VXMtJXHu0s/xBDpTslqsgikbLIxZtZgYMxlLh83j/F3bufJ/BDHxVasgaQmi0DaJpNuCgdG35/cNzli7qYfSxNKUNIyGe2ukS4KC3FGud/7p2RzJQzsAkZ2ZUmqDCbr7+8/whV7F6nJopCGyVBhd5UtzC6TOP9oxd1NarIo1NVkBmbcXzpib6ftspkSlqRNVoZdZQviPdyOv4PUZFGheD+0ihlLZTKZgRfZMlceDqnJotLX13euo6CRhcmOkqZLASab4crDFnndJpsoYam7yYg50C4TM54vmyhhSdJkDMRqabZUEPtSOxdbarIY9Pb2nkAB/+MqbFiV1WSEba614cypJTVZTNjFJfIO0/O8I6XJUtFsNvejBve7cjLiucdkVSUqdTeZgRrc7crJSE2WAAmZrNS3OeawwVzp0ZWXmiwJEjJZqa/2bHaZHHf9wZGX+f7bj2Q1JSoU8iN2YcPKfOAszZUWTHaXKzdehJ+UVZSoUMhYJmMQHjQfOEtzpcVcr7ZNfmqyuFDIuDNZQ5oqPewaL7DzU5MlAIWMa7LZ0lQlIB9zrbOR3DDYo/KwEgeKeTzFjHNC9nBpqjKQ0zSTm5osQSjosBgmrDavX7/+tdJMpeCNwFQ1WYJglkgmYxA8aUJROoNhos5kldtVKimBWdRkSrpw/HGOw0AdxTa9srmidEdNpqROFJM1Go3DZHNF6Y6aTMkEjBb44F93lUok1GRK6oQxmfmSn2ymKMFRkympg8lucRnKFuv1yCaKEg7ME+gdpppMiUxQk5lvkMomihIONZmSOgFNtlJWV5RoYKLtlqlszZRVFSUamKijyVasWKG7SiUeGKmtyVa9cCu/fWVVRYkGJupkshWymqJEByNNchnMiDcGM2Q1RYlOJ5Oh0bKaokSnncn0eExJFHaLl6Gn/Cbj/+XytKIkh+d54zDXT8RkejyWGqNG/R8YjKGM+W8KWwAAAABJRU5ErkJggg=="/> + </g> + </g> + <g clip-path="url(#clip15-61)"> + <g clip-path="url(#clip16-42)"> + <g clip-path="url(#clip17-4)"> + <path transform="matrix(1 0 0 1.0024 1216 205)" d="m43.77 25.533c-11.827 7.476-25.56 21.312-32.461 41.775-5.2 15.42-6.0171 33.834-0.137 51.448 5.2127 15.616 15.546 29.89 29.485 39.598 13.266 9.239 29.358 14.029 45.059 13.584 16.924-0.38 33.006-6.789 44.919-16.918 12.54-10.661 20.332-25.122 23.029-39.234 3.049-15.957-0.167-31.256-6.113-42.76-3.879-7.5046-8.968-13.658-14.185-18.446-5.225-4.7961-10.533-8.1973-15.219-10.642-0.025-0.0131 11.317-22.044 11.281-22.063 0.036 0.0186 11.593-21.901 11.64-21.876 8.696 4.5365 18 11.089 26.661 20.152 8.613 9.0146 16.588 20.543 22.056 34.377 8.348 21.117 10.356 46.884 2.263 71.748-7.174 22.037-22.075 42.043-42.424 55.096-19.367 12.423-42.784 17.865-65.276 15.319-20.842-2.455-40.093-11.686-54.523-25.103-15.173-14.108-24.587-32.328-28.128-50.37-3.9939-20.35-0.5637-39.853 6.3855-55.187 9.1897-20.277 24.038-33.135 35.688-40.5z" fill="#bfbfbf"/> + </g> + </g> + </g> + </g> + </g> +</svg> diff --git a/installer/UNICADOinstaller.py b/installer/UNICADOinstaller.py new file mode 100644 index 0000000000000000000000000000000000000000..29aa0e47e8508a69f9e5265d99cbdd10b9a63abf --- /dev/null +++ b/installer/UNICADOinstaller.py @@ -0,0 +1,588 @@ +# imports for python +import sys +import image_rc +import argparse +from pathlib import Path +from PyQt5 import QtCore, QtGui, QtWidgets +from sub_functions.last_step import last_step +from sub_functions.line_edit import line_edit +from sub_functions.next_step import next_step +from sub_functions.check_tool import check_tool +from sub_functions.update_steps import update_steps +from sub_functions.current_text import current_text +from sub_functions.abort_install import abort_install +from sub_functions.browse_folder import browse_folder +from sub_functions.retranslate_ui import retranslate_ui +from sub_functions.install_unicado import install_unicado, install_unicado_headless +from sub_functions.uninstall_steps import uninstall_steps +from sub_functions.integration_step import integration_step + +""" -*- coding: utf-8 -*- + + Form implementation generated from reading ui file 'UNICADOinstaller.ui' + + Created by: PyQt5 UI code generator 5.15.4 + + WARNING: Any manual changes made to this file will be lost when pyuic5 is + run again. Do not edit this file unless you know what you are doing. +""" + +''' class for gui-layout and action handling ''' +# noinspection PyAttributeOutsideInit + + +class UNICADOworkflowInstaller(object): + def setup_ui(self, unicado_workflow_installer): + unicado_workflow_installer.setObjectName("unicado_workflow_installer") + unicado_workflow_installer.resize(550, 400) + #unicado_workflow_installer.setMinimumSize(550, 400) + #unicado_workflow_installer.setMaximumSize(1920, 1080) + font = QtGui.QFont() + font.setFamily("Calibri") + font.setPointSize(12) + unicado_workflow_installer.setFont(font) + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/newPrefix/unicadoICON.svg"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + unicado_workflow_installer.setWindowIcon(icon) + unicado_workflow_installer.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.Germany)) + + # button panel + self.button_panel = QtWidgets.QWidget(unicado_workflow_installer) + self.button_panel.setGeometry(QtCore.QRect(0, 350, 550, 50)) + self.button_panel.setAutoFillBackground(True) + self.button_panel.setStyleSheet("border-top:3px solid rgb(200, 200, 200)") + self.button_panel.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.Germany)) + self.button_panel.setObjectName("button_panel") + self.integration_button = QtWidgets.QPushButton(self.button_panel) + self.integration_button.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(225, 225, 225);") + self.integration_button.setGeometry(QtCore.QRect(115, 10, 110, 30)) + self.integration_button.setAutoFillBackground(True) + self.integration_button.setObjectName("integration_button") + self.integration_button.released.connect(self.integrationStep) + self.integration_button.setVisible(False) + self.next_button = QtWidgets.QPushButton(self.button_panel) + self.next_button.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(225, 225, 225);") + self.next_button.setGeometry(QtCore.QRect(325, 10, 100, 30)) + self.next_button.setAutoFillBackground(True) + self.next_button.setObjectName("next_button") + self.next_button.released.connect(self.nextStep) + self.cancel_button = QtWidgets.QPushButton(self.button_panel) + self.cancel_button.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(225, 225, 225);") + self.cancel_button.setGeometry(QtCore.QRect(440, 10, 100, 30)) + self.cancel_button.setMouseTracking(False) + self.cancel_button.setAutoFillBackground(True) + self.cancel_button.setObjectName("cancelButton") + self.cancel_button.released.connect(self.abortInstall) + self.back_button = QtWidgets.QPushButton(self.button_panel) + self.back_button.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(225, 225, 225);") + self.back_button.setGeometry(QtCore.QRect(225, 10, 100, 30)) + self.back_button.setAutoFillBackground(True) + self.back_button.setObjectName("back_button") + self.back_button.released.connect(self.lastStep) + self.back_button.setVisible(False) + self.update_button = QtWidgets.QPushButton(self.button_panel) + self.update_button.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(225, 225, 225);") + self.update_button.setGeometry(QtCore.QRect(325, 10, 100, 30)) + self.update_button.setAutoFillBackground(True) + self.update_button.setObjectName("update_button") + self.update_button.released.connect(self.updateSteps) + self.update_button.setVisible(False) + self.uninstall_button = QtWidgets.QPushButton(self.button_panel) + self.uninstall_button.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(225, 225, 225);") + self.uninstall_button.setGeometry(QtCore.QRect(225, 10, 100, 30)) + self.uninstall_button.setAutoFillBackground(True) + self.uninstall_button.setObjectName("uninstall_button") + self.uninstall_button.released.connect(self.uninstallSteps) + self.uninstall_button.setVisible(False) + + # welcome panel + self.welcome_panel = QtWidgets.QWidget(unicado_workflow_installer) + self.welcome_panel.setGeometry(QtCore.QRect(100, 0, 450, 350)) + self.welcome_panel.setAutoFillBackground(True) + self.welcome_panel.setObjectName("welcome_panel") + self.welcome_text_label_1 = QtWidgets.QLabel(self.welcome_panel) + self.welcome_text_label_1.setGeometry(QtCore.QRect(30, 22, 400, 50)) + font = QtGui.QFont() + font.setPointSize(16) + font.setBold(True) + font.setWeight(75) + self.welcome_text_label_1.setFont(font) + self.welcome_text_label_1.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.welcome_text_label_1.setWordWrap(True) + self.welcome_text_label_1.setObjectName("welcome_text_label_1") + self.install_text_label_1 = QtWidgets.QLabel(self.welcome_panel) + self.install_text_label_1.setGeometry(QtCore.QRect(30, 90, 400, 40)) + self.install_text_label_1.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + self.install_text_label_1.setWordWrap(True) + self.install_text_label_1.setObjectName("install_text_label_1") + self.continue_text_label = QtWidgets.QLabel(self.welcome_panel) + self.continue_text_label.setGeometry(QtCore.QRect(30, 190, 400, 20)) + self.continue_text_label.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft) + self.continue_text_label.setObjectName("continue_text_label") + self.install_text_label_2 = QtWidgets.QLabel(self.welcome_panel) + self.install_text_label_2.setGeometry(QtCore.QRect(30, 140, 400, 40)) + self.install_text_label_2.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + self.install_text_label_2.setWordWrap(True) + self.install_text_label_2.setObjectName("install_text_label_2") + + # header panel + self.header_panel = QtWidgets.QWidget(unicado_workflow_installer) + self.header_panel.setGeometry(QtCore.QRect(100, 0, 450, 80)) + self.header_panel.setStyleSheet("border-bottom:3px solid rgb(200, 200, 200)") + self.header_panel.setAutoFillBackground(True) + self.header_panel.setObjectName("header_panel") + self.header_panel.setVisible(False) + self.header_text_label_1 = QtWidgets.QLabel(self.header_panel) + self.header_text_label_1.setGeometry(QtCore.QRect(10, 10, 430, 30)) + font = QtGui.QFont() + font.setPointSize(13) + font.setBold(True) + font.setWeight(75) + self.header_text_label_1.setFont(font) + self.header_text_label_1.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.header_text_label_1.setObjectName("header_text_label_1") + self.header_text_label_2 = QtWidgets.QLabel(self.header_panel) + self.header_text_label_2.setGeometry(QtCore.QRect(20, 30, 410, 30)) + self.header_text_label_2.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.header_text_label_2.setObjectName("header_text_label_2") + + # logo label + self.logo_label = QtWidgets.QLabel(unicado_workflow_installer) + self.logo_label.setGeometry(QtCore.QRect(10, 0, 80, 350)) + self.logo_label.setText("") + self.logo_label.setPixmap(QtGui.QPixmap(":/newPrefix/UNICADOLogo90.svg")) + self.logo_label.setScaledContents(True) + self.logo_label.setAlignment(QtCore.Qt.AlignCenter) + self.logo_label.setObjectName("logo_label") + + # install and finish panel + self.install_and_finish_panel = QtWidgets.QWidget(unicado_workflow_installer) + self.install_and_finish_panel.setGeometry(QtCore.QRect(100, 80, 450, 270)) + self.install_and_finish_panel.setAutoFillBackground(True) + self.install_and_finish_panel.setObjectName("install_and_finish_panel") + self.install_and_finish_panel.setVisible(False) + self.install_progress_bar = QtWidgets.QProgressBar(self.install_and_finish_panel) + self.install_progress_bar.setGeometry(QtCore.QRect(20, 145, 410, 25)) + self.install_progress_bar.setProperty("value", 0) + self.install_progress_bar.setObjectName("install_progress_bar") + self.install_and_finish_panel_text_label_1 = QtWidgets.QLabel(self.install_and_finish_panel) + self.install_and_finish_panel_text_label_1.setGeometry(QtCore.QRect(20, 20, 410, 80)) + self.install_and_finish_panel_text_label_1.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.install_and_finish_panel_text_label_1.setObjectName("install_and_finish_panel_text_label_1") + self.install_and_finish_panel_text_label_2 = QtWidgets.QLabel(self.install_and_finish_panel) + self.install_and_finish_panel_text_label_2.setGeometry(QtCore.QRect(20, 40, 410, 80)) + self.install_and_finish_panel_text_label_2.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.install_and_finish_panel_text_label_2.setObjectName("install_and_finish_panel_text_label_2") + + # repository path panel + self.repository_path_panel = QtWidgets.QWidget(unicado_workflow_installer) + self.repository_path_panel.setGeometry(QtCore.QRect(100, 80, 450, 270)) + self.repository_path_panel.setAutoFillBackground(True) + self.repository_path_panel.setObjectName("repository_path_panel") + self.repository_path_panel.setVisible(False) + + # repository browse panel + self.repository_browse_panel = QtWidgets.QWidget(self.repository_path_panel) + self.repository_browse_panel.setStyleSheet("border:1px solid rgb(200, 200, 200)") + self.repository_browse_panel.setGeometry(QtCore.QRect(20, 130, 410, 55)) + self.repository_browse_panel.setObjectName("repository_browse_panel") + self.repository_path_line_edit = QtWidgets.QLineEdit(self.repository_browse_panel) + self.repository_path_line_edit.setGeometry(QtCore.QRect(15, 15, 295, 25)) + self.repository_path_line_edit.setObjectName("repository_path_line_edit") + self.repository_path_line_edit.textEdited.connect(self.lineEdit) + font = QtGui.QFont() + font.setPointSize(10) + self.repository_path_browse_button = QtWidgets.QPushButton(self.repository_browse_panel) + self.repository_path_browse_button.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(225, 225, 225);") + self.repository_path_browse_button.setGeometry(QtCore.QRect(315, 15, 80, 25)) + self.repository_path_browse_button.setFont(font) + self.repository_path_browse_button.setAutoFillBackground(True) + self.repository_path_browse_button.setObjectName("repository_path_browse_button") + self.repository_path_browse_button.released.connect(self.browseFolder) + self.repository_path_text_label_1 = QtWidgets.QLabel(self.repository_path_panel) + self.repository_path_text_label_1.setGeometry(QtCore.QRect(20, 20, 410, 80)) + self.repository_path_text_label_1.setAutoFillBackground(True) + self.repository_path_text_label_1.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.repository_path_text_label_1.setWordWrap(True) + self.repository_path_text_label_1.setObjectName("repository_path_text_label_1") + self.repository_path_text_label_2 = QtWidgets.QLabel(self.repository_path_panel) + self.repository_path_text_label_2.setGeometry(QtCore.QRect(30, 110, 102, 30)) + self.repository_path_text_label_2.setAutoFillBackground(True) + self.repository_path_text_label_2.setFont(font) + self.repository_path_text_label_2.setAlignment( + QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft) + self.repository_path_text_label_2.setObjectName("repository_path_text_label_2") + + # install path panel + self.install_path_panel = QtWidgets.QWidget(unicado_workflow_installer) + self.install_path_panel.setGeometry(QtCore.QRect(100, 80, 450, 270)) + self.install_path_panel.setAutoFillBackground(True) + self.install_path_panel.setObjectName("install_path_panel") + self.install_path_panel.setVisible(False) + self.install_path_text_label_1 = QtWidgets.QLabel(self.install_path_panel) + self.install_path_text_label_1.setGeometry(QtCore.QRect(20, 20, 410, 80)) + self.install_path_text_label_1.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.install_path_text_label_1.setWordWrap(True) + self.install_path_text_label_1.setObjectName("install_path_text_label_1") + self.install_path_text_label_2 = QtWidgets.QLabel(self.install_path_panel) + self.install_path_text_label_2.setGeometry(QtCore.QRect(20, 210, 410, 30)) + self.install_path_text_label_2.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + self.install_path_text_label_2.setObjectName("install_path_text_label_2") + self.install_path_text_label_3 = QtWidgets.QLabel(self.install_path_panel) + self.install_path_text_label_3.setGeometry(QtCore.QRect(20, 230, 410, 30)) + self.install_path_text_label_3.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) + self.install_path_text_label_3.setObjectName("install_path_text_label_3") + self.install_path_text_label_4 = QtWidgets.QLabel(self.install_path_panel) + self.install_path_text_label_4.setAutoFillBackground(True) + self.install_path_text_label_4.setGeometry(QtCore.QRect(30, 107, 108, 15)) + self.install_path_text_label_4.setFont(font) + self.install_path_text_label_4.setAlignment( + QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft) + self.install_path_text_label_4.setObjectName("install_path_text_label_4") + self.install_radio_button_repo = QtWidgets.QRadioButton('Install from repository', self.install_path_panel) + self.install_radio_button_repo.move(60, 180) + self.install_radio_button_repo.toggled.connect(self.install_mode) + self.install_radio_button_repo.setVisible(False) + self.install_radio_button_alone = QtWidgets.QRadioButton('Install standalone', self.install_path_panel) + self.install_radio_button_alone.move(240, 180) + self.install_radio_button_alone.toggled.connect(self.install_mode) + self.install_radio_button_alone.setChecked(True) + self.install_radio_button_alone.setVisible(False) + self.install_button_group = QtWidgets.QButtonGroup(self.install_path_panel) + self.install_button_group.addButton(self.install_radio_button_repo) + self.install_button_group.addButton(self.install_radio_button_alone) + + # install browse panel + self.install_browse_panel = QtWidgets.QWidget(self.install_path_panel) + self.install_browse_panel.setStyleSheet("border:1px solid rgb(200, 200, 200)") + self.install_browse_panel.setGeometry(QtCore.QRect(20, 115, 410, 55)) + self.install_browse_panel.setObjectName("install_browse_panel") + self.install_path_line_edit = QtWidgets.QLineEdit(self.install_browse_panel) + self.install_path_line_edit.setGeometry(QtCore.QRect(15, 15, 310, 25)) + self.install_path_line_edit.setFont(font) + self.install_path_line_edit.setObjectName("install_path_line_edit") + self.install_path_line_edit.textEdited.connect(self.lineEdit) + self.install_path_browse_button = QtWidgets.QPushButton(self.install_browse_panel) + self.install_path_browse_button.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(225, 225, 225);") + self.install_path_browse_button.setGeometry(QtCore.QRect(315, 15, 80, 25)) + self.install_path_browse_button.setFont(font) + self.install_path_browse_button.setAutoFillBackground(True) + self.install_path_browse_button.setObjectName("install_path_browse_button") + self.install_path_browse_button.released.connect(self.browseFolder) + + # module integration panel + self.integration_panel = QtWidgets.QWidget(unicado_workflow_installer) + self.integration_panel.setGeometry(QtCore.QRect(100, 80, 450, 270)) + self.integration_panel.setAutoFillBackground(True) + self.integration_panel.setObjectName("install_path_panel") + self.integration_panel.setVisible(False) + self.integration_text_label_0 = QtWidgets.QLabel(self.integration_panel) + self.integration_text_label_0.setGeometry(QtCore.QRect(20, 20, 410, 80)) + self.integration_text_label_0.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.integration_text_label_0.setWordWrap(True) + self.integration_text_label_0.setObjectName("integration_text_label_0") + self.integration_text_label_0.setVisible(False) + self.integration_text_label_0.setEnabled(False) + self.integration_text_label_1 = QtWidgets.QLabel(self.integration_panel) + self.integration_text_label_1.setGeometry(QtCore.QRect(20, 20, 410, 80)) + self.integration_text_label_1.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.integration_text_label_1.setWordWrap(True) + self.integration_text_label_1.setObjectName("integration_text_label_1") + self.integration_text_label_2 = QtWidgets.QLabel(self.integration_panel) + self.integration_text_label_2.setGeometry(QtCore.QRect(20, 170, 410, 80)) + self.integration_text_label_2.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.integration_text_label_2.setWordWrap(True) + self.integration_text_label_2.setObjectName("integration_text_label_2") + self.integration_text_label_2.setVisible(False) + self.integration_text_label_3 = QtWidgets.QLabel(self.integration_panel) + self.integration_text_label_3.setGeometry(QtCore.QRect(20, 125, 410, 80)) + self.integration_text_label_3.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.integration_text_label_3.setWordWrap(True) + self.integration_text_label_3.setObjectName("integration_text_label_3") + self.integration_text_label_3.setVisible(False) + self.integration_text_label_4 = QtWidgets.QLabel(self.integration_panel) + self.integration_text_label_4.setGeometry(QtCore.QRect(20, 200, 410, 80)) + self.integration_text_label_4.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.integration_text_label_4.setWordWrap(True) + self.integration_text_label_4.setObjectName("integration_text_label_4") + self.integration_text_label_4.setVisible(False) + self.integration_text_label_5 = QtWidgets.QLabel(self.integration_panel) + self.integration_text_label_5.setGeometry(QtCore.QRect(20, 125, 410, 80)) + self.integration_text_label_5.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.integration_text_label_5.setWordWrap(True) + self.integration_text_label_5.setObjectName("integration_text_label_5") + self.integration_text_label_5.setVisible(False) + self.tool_name_panel = QtWidgets.QWidget(self.integration_panel) + self.tool_name_panel.setStyleSheet("border:1px solid rgb(200, 200, 200)") + self.tool_name_panel.setGeometry(QtCore.QRect(20, 107, 410, 55)) + self.tool_name_panel.setObjectName("tool_name_panel") + self.tool_name_panel_edit = QtWidgets.QLineEdit(self.tool_name_panel) + self.tool_name_panel_edit.setGeometry(QtCore.QRect(15, 15, 295, 25)) + self.tool_name_panel_edit.setFont(font) + self.tool_name_panel_edit.setObjectName("tool_name_panel_edit") + self.tool_name_panel_edit.textEdited.connect(self.lineEdit) + self.tool_name_panel_edit_button = QtWidgets.QPushButton(self.tool_name_panel) + self.tool_name_panel_edit_button.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(225, 225, 225);") + self.tool_name_panel_edit_button.setGeometry(QtCore.QRect(315, 15, 80, 25)) + self.tool_name_panel_edit_button.setFont(font) + self.tool_name_panel_edit_button.setAutoFillBackground(True) + self.tool_name_panel_edit_button.setObjectName("tool_name_panel_edit_button") + self.tool_name_panel_edit_button.released.connect(self.checkTool) + + self.button_name_panel = QtWidgets.QWidget(self.integration_panel) + self.button_name_panel.setGeometry(QtCore.QRect(20, 230, 410, 55)) + self.button_name_panel.setObjectName("button_name_panel") + self.button_name_panel.setVisible(False) + self.integration_radio_button_delete = QtWidgets.QRadioButton('Delete Tool', self.button_name_panel) + self.integration_radio_button_delete.move(10, 10) + self.integration_radio_button_delete.toggled.connect(self.deleteOrOverwriteTool) + self.integration_radio_button_overwrite = QtWidgets.QRadioButton('Overwrite Tool', self.button_name_panel) + self.integration_radio_button_overwrite.move(150, 10) + self.integration_radio_button_overwrite.toggled.connect(self.deleteOrOverwriteTool) + self.integration_button_group_name = QtWidgets.QButtonGroup(self.button_name_panel) + self.integration_button_group_name.addButton(self.integration_radio_button_delete) + self.integration_button_group_name.addButton(self.integration_radio_button_overwrite) + + self.group_name_panel = QtWidgets.QWidget(self.integration_panel) + self.group_name_panel.setStyleSheet("border:1px solid rgb(200, 200, 200)") + self.group_name_panel.setGeometry(QtCore.QRect(20, 173, 410, 55)) + self.group_name_panel.setObjectName("group_name_panel") + self.group_name_panel_edit = QtWidgets.QLineEdit(self.group_name_panel) + self.group_name_panel_edit.setGeometry(QtCore.QRect(15, 15, 295, 25)) + self.group_name_panel_edit.setFont(font) + self.group_name_panel_edit.setObjectName("group_name_panel_edit") + self.group_name_panel_edit.textEdited.connect(self.lineEdit) + self.group_name_panel_edit_button = QtWidgets.QPushButton(self.group_name_panel) + self.group_name_panel_edit_button.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(225, 225, 225);") + self.group_name_panel_edit_button.setGeometry(QtCore.QRect(315, 15, 80, 25)) + self.group_name_panel_edit_button.setFont(font) + self.group_name_panel_edit_button.setAutoFillBackground(True) + self.group_name_panel_edit_button.setObjectName("group_name_panel_edit_button") + self.group_name_panel_edit_button.released.connect(self.checkTool) + self.group_name_panel.setVisible(False) + self.tool_path_panel = QtWidgets.QWidget(self.integration_panel) + self.tool_path_panel.setStyleSheet("border:1px solid rgb(200, 200, 200)") + self.tool_path_panel.setGeometry(QtCore.QRect(20, 50, 410, 55)) + self.tool_path_panel.setObjectName("tool_path_panel") + self.tool_path_panel.setVisible(False) + self.tool_path_panel_edit = QtWidgets.QLineEdit(self.tool_path_panel) + self.tool_path_panel_edit.setGeometry(QtCore.QRect(15, 15, 295, 25)) + self.tool_path_panel_edit.setFont(font) + self.tool_path_panel_edit.setObjectName("tool_path_panel_edit") + self.tool_path_panel_edit.textEdited.connect(self.lineEdit) + self.tool_path_panel_edit.setEnabled(False) + self.tool_path_panel_browse_button = QtWidgets.QPushButton(self.tool_path_panel) + self.tool_path_panel_browse_button.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(225, 225, 225);") + self.tool_path_panel_browse_button.setGeometry(QtCore.QRect(315, 15, 80, 25)) + self.tool_path_panel_browse_button.setFont(font) + self.tool_path_panel_browse_button.setAutoFillBackground(True) + self.tool_path_panel_browse_button.setObjectName("tool_path_panel_browse_button") + self.tool_path_panel_browse_button.released.connect(self.browseFolder) + self.integration_combo_box = QtWidgets.QComboBox(self.integration_panel) + self.integration_combo_box.setGeometry(QtCore.QRect(20, 150, 410, 25)) + self.integration_combo_box.addItems(['- please select a tool group -', 'postProcessing', 'preSizing', + 'sizingLoop', 'visualization', '- other -']) + self.integration_combo_box.activated.connect(self.currentText) + self.integration_combo_box.setVisible(False) + self.button_group_panel = QtWidgets.QWidget(self.integration_panel) + self.button_group_panel.setGeometry(QtCore.QRect(20, 50, 410, 55)) + self.button_group_panel.setObjectName("button_group_panel") + self.button_group_panel.setVisible(False) + self.integration_radio_button_yes = QtWidgets.QRadioButton('Yes', self.button_group_panel) + self.integration_radio_button_yes.move(10, 15) + self.integration_radio_button_yes.toggled.connect(self.start_integration) + self.integration_radio_button_no = QtWidgets.QRadioButton('No', self.button_group_panel) + self.integration_radio_button_no.move(10, 35) + self.integration_radio_button_no.toggled.connect(self.start_integration) + self.integration_button_group = QtWidgets.QButtonGroup(self.button_group_panel) + self.integration_button_group.addButton(self.integration_radio_button_yes) + self.integration_button_group.addButton(self.integration_radio_button_no) + self.checkbox_panel = QtWidgets.QWidget(self.integration_panel) + self.checkbox_panel.setGeometry(QtCore.QRect(20, 178, 410, 55)) + self.checkbox_panel.setObjectName("checkbox_panel") + self.checkbox_panel.setVisible(False) + self.integration_checkbox = QtWidgets.QCheckBox('Repository integration', self.checkbox_panel) + self.integration_checkbox.move(10, 10) + self.integration_checkbox.setChecked(False) + self.integration_checkbox.setVisible(False) + self.integration_checkbox.toggled.connect(self.enterPassword) + self.checkbox_panel_edit = QtWidgets.QLineEdit(self.checkbox_panel) + self.checkbox_panel_edit.setGeometry(QtCore.QRect(210, 10, 125, 25)) + self.checkbox_panel_edit.setStyleSheet("border:1px solid rgb(200, 200, 200); " + "Background-color: rgb(255, 255, 255);") + self.checkbox_panel_edit.setFont(font) + self.checkbox_panel_edit.setObjectName("checkbox_panel_edit") + self.checkbox_panel_edit.textEdited.connect(self.lineEdit) + self.checkbox_panel_edit.setVisible(False) + self.checkbox_panel_text_label_0 = QtWidgets.QLabel(self.checkbox_panel) + self.checkbox_panel_text_label_0.setGeometry(QtCore.QRect(20, 20, 410, 80)) + self.checkbox_panel_text_label_0.setAlignment( + QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.checkbox_panel_text_label_0.setWordWrap(True) + self.checkbox_panel_text_label_0.setObjectName("checkbox_panel_text_label_0") + self.checkbox_panel_text_label_0.setVisible(False) + self.checkbox_panel_text_label_0.setEnabled(False) + self.integration_checkbox_show = QtWidgets.QCheckBox('show', self.checkbox_panel) + self.integration_checkbox_show.move(340, 10) + self.integration_checkbox_show.setChecked(False) + self.integration_checkbox_show.setVisible(False) + self.integration_checkbox_show.toggled.connect(self.show_input) + + self.button_panel.raise_() + self.logo_label.raise_() + self.repository_path_panel.raise_() + self.install_path_panel.raise_() + self.integration_panel.raise_() + self.install_and_finish_panel.raise_() + self.header_panel.raise_() + self.welcome_panel.raise_() + + # show gui with all settings set above + unicado_workflow_installer.show() + + # function call to set text to all elements + self.retranslateUi(unicado_workflow_installer) + QtCore.QMetaObject.connectSlotsByName(unicado_workflow_installer) + + # function to set module integration button settings + def integrationStep(self): + # call function to handle the next step button + integration_step(self) + + # function to set properties of next step button + def nextStep(self): + # call function to handle the next step button + next_step(self) + + # function for back button + def lastStep(self): + # call function to handle the back button + last_step(self) + + # function for abort installation + def abortInstall(self): + # call function to handle the cancel button + abort_install(self) + + # function to update unicado to current version + def updateSteps(self): + # call function to handle the update button + update_steps(self) + + # function to uninstall the entire unicado workflow + def uninstallSteps(self): + # call function to handle the uninstallation button + uninstall_steps(self) + + # function for browse buttons + def browseFolder(self): + browse_folder(self) + + # function to handle user inputs by line edit field + def lineEdit(self): + line_edit(self) + + # function to check given tool name to be integrated + def checkTool(self): + check_tool(self) + + # function to delete or overwrite an integrated non-basic tool + def deleteOrOverwriteTool(self): + self.next_button.setEnabled(True) + + # function to display required disk space dependent on installation mode + def install_mode(self): + # check which installation mode is selected + if self.install_radio_button_repo.isChecked(): + self.install_path_text_label_2.setText("Disk space required: 0.85 GB") + else: + self.install_path_text_label_2.setText("Disk space required: 1.75 GB") + + # function to make user input visible + def show_input(self): + if self.integration_checkbox_show.isChecked(): + self.checkbox_panel_edit.setText(self.checkbox_panel_text_label_0.text()) + else: + stars = '' + for _ in self.checkbox_panel_edit.text(): + stars += '*' + self.checkbox_panel_edit.setText(stars) + + # function to get the selected string from combo box + def currentText(self): + current_text(self) + + # function to activate password entering + def enterPassword(self): + if self.integration_checkbox.isChecked(): + self.checkbox_panel_edit.setVisible(True) + if not self.checkbox_panel_edit.text() == 'enter password': + self.integration_checkbox_show.setVisible(True) + if self.integration_radio_button_yes.isChecked() or self.integration_radio_button_no.isChecked(): + self.next_button.setEnabled(False) + if self.integration_text_label_0.text() == self.checkbox_panel_text_label_0.text(): + self.next_button.setEnabled(True) + else: + self.checkbox_panel_edit.setVisible(False) + self.integration_checkbox_show.setVisible(False) + if self.integration_radio_button_yes.isChecked() or self.integration_radio_button_no.isChecked(): + self.next_button.setEnabled(True) + + # function to activate the start integration button + def start_integration(self): + if self.integration_checkbox.isChecked(): + if self.integration_text_label_0.text() == self.checkbox_panel_text_label_0.text() and \ + (self.integration_radio_button_yes.isChecked() or self.integration_radio_button_no.isChecked()): + self.next_button.setEnabled(True) + else: + self.next_button.setEnabled(True) + + # function to install UNICADOworkflow on system + def installUNICADO(self): + install_unicado(self) + + # set text to text-fields + def retranslateUi(self, UNICADOworkflow_installer): + retranslate_ui(self, UNICADOworkflow_installer) + +''' main function für installer gui ''' +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Unicado Installer") + parser.add_argument( + '--install-dir', + type=Path, + help='Install path' + ) + args = parser.parse_args() + if args.install_dir is None: + print("no argumens") + app = QtWidgets.QApplication(sys.argv) + UNICADOworkflow_installer = QtWidgets.QFrame() + ui = UNICADOworkflowInstaller() + ui.setup_ui(UNICADOworkflow_installer) + sys.exit(app.exec_()) + else: + install_unicado_headless(args.install_dir) diff --git a/installer/iconUNICADO.ico b/installer/iconUNICADO.ico new file mode 100644 index 0000000000000000000000000000000000000000..bb960a72aa445e12d49c0fed4ae52e8497cdddac Binary files /dev/null and b/installer/iconUNICADO.ico differ diff --git a/installer/image_rc.py b/installer/image_rc.py new file mode 100644 index 0000000000000000000000000000000000000000..2824d89e7967f0acee80f777a268da3e0aa2d269 --- /dev/null +++ b/installer/image_rc.py @@ -0,0 +1,1196 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x25\x6b\ +\x3c\ +\x73\x76\x67\x20\x77\x69\x64\x74\x68\x3d\x22\x33\x38\x22\x20\x68\ +\x65\x69\x67\x68\x74\x3d\x22\x33\x38\x22\x20\x78\x6d\x6c\x6e\x73\ +\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\ +\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x78\x6d\ +\x6c\x6e\x73\x3a\x78\x6c\x69\x6e\x6b\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\ +\x39\x2f\x78\x6c\x69\x6e\x6b\x22\x20\x6f\x76\x65\x72\x66\x6c\x6f\ +\x77\x3d\x22\x68\x69\x64\x64\x65\x6e\x22\x3e\x3c\x64\x65\x66\x73\ +\x3e\x3c\x63\x6c\x69\x70\x50\x61\x74\x68\x20\x69\x64\x3d\x22\x63\ +\x6c\x69\x70\x30\x22\x3e\x3c\x72\x65\x63\x74\x20\x78\x3d\x22\x36\ +\x31\x38\x22\x20\x79\x3d\x22\x32\x36\x36\x22\x20\x77\x69\x64\x74\ +\x68\x3d\x22\x33\x38\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x33\ +\x38\x22\x2f\x3e\x3c\x2f\x63\x6c\x69\x70\x50\x61\x74\x68\x3e\x3c\ +\x63\x6c\x69\x70\x50\x61\x74\x68\x20\x69\x64\x3d\x22\x63\x6c\x69\ +\x70\x31\x22\x3e\x3c\x72\x65\x63\x74\x20\x78\x3d\x22\x36\x31\x39\ +\x22\x20\x79\x3d\x22\x32\x36\x37\x22\x20\x77\x69\x64\x74\x68\x3d\ +\x22\x33\x36\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x33\x36\x22\ +\x2f\x3e\x3c\x2f\x63\x6c\x69\x70\x50\x61\x74\x68\x3e\x3c\x63\x6c\ +\x69\x70\x50\x61\x74\x68\x20\x69\x64\x3d\x22\x63\x6c\x69\x70\x32\ +\x22\x3e\x3c\x72\x65\x63\x74\x20\x78\x3d\x22\x36\x31\x39\x22\x20\ +\x79\x3d\x22\x32\x36\x37\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x33\ +\x36\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x33\x36\x22\x2f\x3e\ +\x3c\x2f\x63\x6c\x69\x70\x50\x61\x74\x68\x3e\x3c\x63\x6c\x69\x70\ +\x50\x61\x74\x68\x20\x69\x64\x3d\x22\x63\x6c\x69\x70\x33\x22\x3e\ +\x3c\x72\x65\x63\x74\x20\x78\x3d\x22\x36\x31\x39\x22\x20\x79\x3d\ +\x22\x32\x36\x38\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x33\x35\x22\ +\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x33\x34\x22\x2f\x3e\x3c\x2f\ +\x63\x6c\x69\x70\x50\x61\x74\x68\x3e\x3c\x63\x6c\x69\x70\x50\x61\ +\x74\x68\x20\x69\x64\x3d\x22\x63\x6c\x69\x70\x34\x22\x3e\x3c\x72\ +\x65\x63\x74\x20\x78\x3d\x22\x36\x31\x39\x22\x20\x79\x3d\x22\x32\ +\x36\x38\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x33\x35\x22\x20\x68\ +\x65\x69\x67\x68\x74\x3d\x22\x33\x34\x22\x2f\x3e\x3c\x2f\x63\x6c\ +\x69\x70\x50\x61\x74\x68\x3e\x3c\x63\x6c\x69\x70\x50\x61\x74\x68\ +\x20\x69\x64\x3d\x22\x63\x6c\x69\x70\x35\x22\x3e\x3c\x72\x65\x63\ +\x74\x20\x78\x3d\x22\x36\x31\x39\x22\x20\x79\x3d\x22\x32\x36\x38\ +\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x33\x35\x22\x20\x68\x65\x69\ +\x67\x68\x74\x3d\x22\x33\x34\x22\x2f\x3e\x3c\x2f\x63\x6c\x69\x70\ +\x50\x61\x74\x68\x3e\x3c\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\ +\x69\x65\x6e\x74\x20\x78\x31\x3d\x22\x31\x31\x33\x2e\x33\x31\x39\ +\x22\x20\x79\x31\x3d\x22\x31\x33\x35\x2e\x37\x31\x33\x22\x20\x78\ +\x32\x3d\x22\x39\x38\x2e\x38\x33\x32\x37\x22\x20\x79\x32\x3d\x22\ +\x31\x35\x30\x2e\x31\x39\x39\x22\x20\x67\x72\x61\x64\x69\x65\x6e\ +\x74\x55\x6e\x69\x74\x73\x3d\x22\x75\x73\x65\x72\x53\x70\x61\x63\ +\x65\x4f\x6e\x55\x73\x65\x22\x20\x73\x70\x72\x65\x61\x64\x4d\x65\ +\x74\x68\x6f\x64\x3d\x22\x70\x61\x64\x22\x20\x69\x64\x3d\x22\x66\ +\x69\x6c\x6c\x36\x22\x3e\x3c\x73\x74\x6f\x70\x20\x6f\x66\x66\x73\ +\x65\x74\x3d\x22\x30\x22\x20\x73\x74\x6f\x70\x2d\x63\x6f\x6c\x6f\ +\x72\x3d\x22\x23\x36\x43\x36\x43\x36\x43\x22\x2f\x3e\x3c\x73\x74\ +\x6f\x70\x20\x6f\x66\x66\x73\x65\x74\x3d\x22\x31\x22\x2f\x3e\x3c\ +\x2f\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x3e\ +\x3c\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x20\ +\x78\x31\x3d\x22\x31\x30\x37\x2e\x31\x39\x38\x22\x20\x79\x31\x3d\ +\x22\x31\x34\x34\x2e\x36\x34\x38\x22\x20\x78\x32\x3d\x22\x38\x37\ +\x2e\x39\x34\x37\x31\x22\x20\x79\x32\x3d\x22\x31\x34\x38\x2e\x36\ +\x32\x39\x22\x20\x67\x72\x61\x64\x69\x65\x6e\x74\x55\x6e\x69\x74\ +\x73\x3d\x22\x75\x73\x65\x72\x53\x70\x61\x63\x65\x4f\x6e\x55\x73\ +\x65\x22\x20\x73\x70\x72\x65\x61\x64\x4d\x65\x74\x68\x6f\x64\x3d\ +\x22\x70\x61\x64\x22\x20\x69\x64\x3d\x22\x66\x69\x6c\x6c\x37\x22\ +\x3e\x3c\x73\x74\x6f\x70\x20\x6f\x66\x66\x73\x65\x74\x3d\x22\x30\ +\x22\x20\x73\x74\x6f\x70\x2d\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x30\ +\x30\x34\x32\x37\x44\x22\x2f\x3e\x3c\x73\x74\x6f\x70\x20\x6f\x66\ +\x66\x73\x65\x74\x3d\x22\x31\x22\x20\x73\x74\x6f\x70\x2d\x63\x6f\ +\x6c\x6f\x72\x3d\x22\x23\x30\x30\x37\x44\x45\x43\x22\x2f\x3e\x3c\ +\x2f\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x3e\ +\x3c\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x20\ +\x78\x31\x3d\x22\x39\x32\x2e\x34\x36\x34\x39\x22\x20\x79\x31\x3d\ +\x22\x31\x33\x30\x2e\x39\x33\x31\x22\x20\x78\x32\x3d\x22\x31\x30\ +\x32\x2e\x31\x35\x36\x22\x20\x79\x32\x3d\x22\x31\x34\x33\x2e\x39\ +\x33\x22\x20\x67\x72\x61\x64\x69\x65\x6e\x74\x55\x6e\x69\x74\x73\ +\x3d\x22\x75\x73\x65\x72\x53\x70\x61\x63\x65\x4f\x6e\x55\x73\x65\ +\x22\x20\x73\x70\x72\x65\x61\x64\x4d\x65\x74\x68\x6f\x64\x3d\x22\ +\x70\x61\x64\x22\x20\x69\x64\x3d\x22\x66\x69\x6c\x6c\x38\x22\x3e\ +\x3c\x73\x74\x6f\x70\x20\x6f\x66\x66\x73\x65\x74\x3d\x22\x30\x22\ +\x20\x73\x74\x6f\x70\x2d\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x45\x32\ +\x30\x45\x31\x46\x22\x2f\x3e\x3c\x73\x74\x6f\x70\x20\x6f\x66\x66\ +\x73\x65\x74\x3d\x22\x31\x22\x20\x73\x74\x6f\x70\x2d\x63\x6f\x6c\ +\x6f\x72\x3d\x22\x23\x46\x32\x34\x36\x35\x37\x22\x2f\x3e\x3c\x2f\ +\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x3e\x3c\ +\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x20\x78\ +\x31\x3d\x22\x31\x32\x30\x2e\x32\x30\x31\x22\x20\x79\x31\x3d\x22\ +\x31\x33\x35\x2e\x35\x36\x31\x22\x20\x78\x32\x3d\x22\x31\x30\x30\ +\x2e\x35\x34\x34\x22\x20\x79\x32\x3d\x22\x31\x33\x30\x2e\x35\x30\ +\x34\x22\x20\x67\x72\x61\x64\x69\x65\x6e\x74\x55\x6e\x69\x74\x73\ +\x3d\x22\x75\x73\x65\x72\x53\x70\x61\x63\x65\x4f\x6e\x55\x73\x65\ +\x22\x20\x73\x70\x72\x65\x61\x64\x4d\x65\x74\x68\x6f\x64\x3d\x22\ +\x70\x61\x64\x22\x20\x69\x64\x3d\x22\x66\x69\x6c\x6c\x39\x22\x3e\ +\x3c\x73\x74\x6f\x70\x20\x6f\x66\x66\x73\x65\x74\x3d\x22\x30\x22\ +\x20\x73\x74\x6f\x70\x2d\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x45\x41\ +\x37\x44\x39\x32\x22\x2f\x3e\x3c\x73\x74\x6f\x70\x20\x6f\x66\x66\ +\x73\x65\x74\x3d\x22\x31\x22\x20\x73\x74\x6f\x70\x2d\x63\x6f\x6c\ +\x6f\x72\x3d\x22\x23\x41\x39\x31\x42\x33\x36\x22\x20\x73\x74\x6f\ +\x70\x2d\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x2e\x38\x39\x30\ +\x31\x39\x36\x22\x2f\x3e\x3c\x2f\x6c\x69\x6e\x65\x61\x72\x47\x72\ +\x61\x64\x69\x65\x6e\x74\x3e\x3c\x63\x6c\x69\x70\x50\x61\x74\x68\ +\x20\x69\x64\x3d\x22\x63\x6c\x69\x70\x31\x30\x22\x3e\x3c\x72\x65\ +\x63\x74\x20\x78\x3d\x22\x36\x32\x38\x22\x20\x79\x3d\x22\x32\x36\ +\x38\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x36\x22\x20\x68\x65\ +\x69\x67\x68\x74\x3d\x22\x31\x37\x22\x2f\x3e\x3c\x2f\x63\x6c\x69\ +\x70\x50\x61\x74\x68\x3e\x3c\x63\x6c\x69\x70\x50\x61\x74\x68\x20\ +\x69\x64\x3d\x22\x63\x6c\x69\x70\x31\x31\x22\x3e\x3c\x72\x65\x63\ +\x74\x20\x78\x3d\x22\x36\x32\x38\x22\x20\x79\x3d\x22\x32\x36\x38\ +\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x36\x22\x20\x68\x65\x69\ +\x67\x68\x74\x3d\x22\x31\x37\x22\x2f\x3e\x3c\x2f\x63\x6c\x69\x70\ +\x50\x61\x74\x68\x3e\x3c\x63\x6c\x69\x70\x50\x61\x74\x68\x20\x69\ +\x64\x3d\x22\x63\x6c\x69\x70\x31\x32\x22\x3e\x3c\x72\x65\x63\x74\ +\x20\x78\x3d\x22\x36\x32\x38\x22\x20\x79\x3d\x22\x32\x36\x38\x22\ +\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x36\x22\x20\x68\x65\x69\x67\ +\x68\x74\x3d\x22\x31\x37\x22\x2f\x3e\x3c\x2f\x63\x6c\x69\x70\x50\ +\x61\x74\x68\x3e\x3c\x63\x6c\x69\x70\x50\x61\x74\x68\x20\x69\x64\ +\x3d\x22\x63\x6c\x69\x70\x31\x33\x22\x3e\x3c\x72\x65\x63\x74\x20\ +\x78\x3d\x22\x2d\x34\x2e\x33\x37\x39\x37\x35\x22\x20\x79\x3d\x22\ +\x2d\x30\x2e\x33\x31\x38\x36\x37\x22\x20\x77\x69\x64\x74\x68\x3d\ +\x22\x31\x31\x33\x2e\x33\x36\x31\x22\x20\x68\x65\x69\x67\x68\x74\ +\x3d\x22\x31\x31\x39\x2e\x30\x36\x35\x22\x2f\x3e\x3c\x2f\x63\x6c\ +\x69\x70\x50\x61\x74\x68\x3e\x3c\x69\x6d\x61\x67\x65\x20\x77\x69\ +\x64\x74\x68\x3d\x22\x35\x34\x22\x20\x68\x65\x69\x67\x68\x74\x3d\ +\x22\x35\x39\x22\x20\x78\x6c\x69\x6e\x6b\x3a\x68\x72\x65\x66\x3d\ +\x22\x64\x61\x74\x61\x3a\x69\x6d\x61\x67\x65\x2f\x70\x6e\x67\x3b\ +\x62\x61\x73\x65\x36\x34\x2c\x69\x56\x42\x4f\x52\x77\x30\x4b\x47\ +\x67\x6f\x41\x41\x41\x41\x4e\x53\x55\x68\x45\x55\x67\x41\x41\x41\ +\x41\x38\x41\x41\x41\x41\x52\x43\x41\x4d\x41\x41\x41\x41\x31\x31\ +\x41\x61\x54\x41\x41\x41\x41\x41\x58\x4e\x53\x52\x30\x49\x41\x72\ +\x73\x34\x63\x36\x51\x41\x41\x41\x41\x52\x6e\x51\x55\x31\x42\x41\ +\x41\x43\x78\x6a\x77\x76\x38\x59\x51\x55\x41\x41\x41\x45\x49\x55\ +\x45\x78\x55\x52\x51\x41\x41\x41\x50\x2f\x2f\x2f\x34\x44\x2f\x2f\ +\x7a\x50\x4d\x7a\x44\x6e\x47\x78\x6c\x57\x2f\x31\x55\x37\x45\x32\ +\x45\x65\x34\x78\x6c\x48\x46\x30\x54\x57\x31\x74\x53\x6d\x31\x76\ +\x57\x4c\x4c\x32\x43\x79\x6f\x74\x46\x72\x4c\x31\x54\x65\x79\x75\ +\x7a\x2b\x30\x78\x43\x79\x6e\x74\x79\x2b\x70\x75\x31\x43\x39\x7a\ +\x44\x61\x77\x76\x69\x2b\x72\x75\x31\x76\x48\x31\x57\x4c\x4d\x32\ +\x43\x53\x6a\x73\x79\x53\x6c\x74\x43\x69\x6e\x74\x56\x4c\x44\x7a\ +\x32\x66\x52\x33\x57\x6a\x52\x33\x56\x2f\x4b\x31\x7a\x4b\x74\x76\ +\x44\x32\x30\x77\x6a\x71\x79\x77\x47\x44\x4c\x31\x79\x53\x6b\x73\ +\x33\x58\x5a\x35\x53\x65\x6d\x74\x6b\x79\x2f\x7a\x48\x50\x5a\x35\ +\x43\x57\x6b\x74\x43\x65\x6d\x74\x53\x6d\x6e\x74\x69\x71\x6f\x74\ +\x79\x75\x6f\x75\x43\x79\x70\x75\x43\x32\x71\x75\x53\x36\x71\x75\ +\x54\x43\x72\x75\x6a\x4b\x74\x76\x44\x4f\x75\x76\x54\x53\x76\x76\ +\x6a\x61\x77\x76\x7a\x65\x78\x77\x44\x69\x78\x77\x44\x75\x7a\x77\ +\x6a\x36\x31\x78\x44\x2b\x32\x78\x45\x43\x33\x78\x55\x4b\x34\x78\ +\x6b\x53\x35\x78\x30\x61\x37\x79\x45\x65\x37\x79\x55\x69\x38\x79\ +\x6b\x71\x2b\x79\x30\x75\x2b\x79\x30\x32\x2f\x7a\x55\x37\x42\x7a\ +\x6b\x2f\x42\x7a\x6c\x4c\x43\x7a\x31\x54\x45\x30\x56\x58\x46\x30\ +\x6c\x62\x47\x30\x6c\x6a\x48\x31\x46\x72\x49\x31\x56\x76\x4a\x31\ +\x6c\x33\x4b\x31\x31\x2f\x4d\x32\x47\x4c\x4e\x32\x6d\x50\x4f\x32\ +\x6d\x58\x50\x32\x32\x62\x51\x33\x47\x6a\x53\x33\x6d\x6e\x53\x33\ +\x6d\x7a\x55\x34\x47\x2f\x57\x34\x6e\x50\x5a\x35\x48\x54\x5a\x35\ +\x58\x62\x62\x35\x71\x48\x6d\x6c\x6b\x51\x41\x41\x41\x41\x6e\x64\ +\x46\x4a\x4f\x55\x77\x41\x42\x41\x67\x55\x4a\x44\x41\x30\x53\x46\ +\x68\x67\x66\x4a\x79\x6b\x32\x4f\x44\x31\x41\x52\x32\x6c\x36\x67\ +\x34\x79\x52\x76\x39\x6e\x6b\x35\x4f\x6a\x75\x38\x50\x4c\x79\x39\ +\x2f\x6a\x37\x2f\x66\x37\x2b\x2f\x76\x78\x32\x73\x2b\x45\x41\x41\ +\x41\x41\x4a\x63\x45\x68\x5a\x63\x77\x41\x41\x44\x73\x4d\x41\x41\ +\x41\x37\x44\x41\x63\x64\x76\x71\x47\x51\x41\x41\x41\x43\x56\x53\ +\x55\x52\x42\x56\x43\x68\x54\x4e\x63\x6e\x56\x47\x6f\x4a\x51\x45\ +\x49\x58\x52\x45\x62\x75\x37\x4f\x37\x46\x62\x44\x4f\x7a\x75\x56\ +\x74\x37\x2f\x54\x59\x54\x44\x64\x74\x33\x4d\x76\x37\x38\x68\x6c\ +\x53\x4e\x52\x7a\x62\x6e\x51\x52\x4a\x70\x6b\x72\x39\x32\x6f\x46\ +\x45\x4f\x59\x65\x6e\x45\x6f\x64\x46\x76\x31\x4d\x68\x39\x6b\x6b\ +\x31\x74\x4d\x78\x59\x48\x51\x61\x64\x5a\x4b\x76\x45\x58\x5a\x6b\ +\x64\x56\x38\x4d\x75\x71\x6e\x76\x46\x70\x7a\x4f\x43\x4e\x50\x77\ +\x33\x36\x7a\x6e\x49\x32\x64\x79\x6f\x66\x63\x4e\x71\x4c\x34\x63\ +\x62\x66\x4f\x63\x32\x77\x53\x2b\x59\x69\x75\x35\x38\x50\x57\x69\ +\x43\x6d\x7a\x33\x79\x38\x6e\x44\x31\x6f\x52\x65\x4e\x37\x53\x53\ +\x43\x62\x32\x65\x75\x69\x51\x7a\x50\x63\x64\x52\x61\x6d\x6b\x6a\ +\x77\x6d\x6c\x6b\x72\x49\x49\x6b\x4b\x77\x49\x4b\x4f\x44\x2b\x2b\ +\x58\x45\x5a\x6f\x68\x2f\x70\x58\x68\x41\x39\x38\x47\x33\x62\x7a\ +\x77\x41\x41\x41\x41\x42\x4a\x52\x55\x35\x45\x72\x6b\x4a\x67\x67\ +\x67\x3d\x3d\x22\x20\x70\x72\x65\x73\x65\x72\x76\x65\x41\x73\x70\ +\x65\x63\x74\x52\x61\x74\x69\x6f\x3d\x22\x6e\x6f\x6e\x65\x22\x20\ +\x69\x64\x3d\x22\x69\x6d\x67\x31\x34\x22\x3e\x3c\x2f\x69\x6d\x61\ +\x67\x65\x3e\x3c\x63\x6c\x69\x70\x50\x61\x74\x68\x20\x69\x64\x3d\ +\x22\x63\x6c\x69\x70\x31\x35\x22\x3e\x3c\x72\x65\x63\x74\x20\x78\ +\x3d\x22\x30\x22\x20\x79\x3d\x22\x30\x22\x20\x77\x69\x64\x74\x68\ +\x3d\x22\x31\x30\x37\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x31\ +\x31\x36\x2e\x39\x30\x37\x22\x2f\x3e\x3c\x2f\x63\x6c\x69\x70\x50\ +\x61\x74\x68\x3e\x3c\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\ +\x65\x6e\x74\x20\x78\x31\x3d\x22\x31\x32\x32\x2e\x34\x31\x38\x22\ +\x20\x79\x31\x3d\x22\x31\x34\x33\x2e\x34\x38\x32\x22\x20\x78\x32\ +\x3d\x22\x31\x31\x31\x2e\x31\x36\x33\x22\x20\x79\x32\x3d\x22\x31\ +\x33\x31\x2e\x32\x33\x39\x22\x20\x67\x72\x61\x64\x69\x65\x6e\x74\ +\x55\x6e\x69\x74\x73\x3d\x22\x75\x73\x65\x72\x53\x70\x61\x63\x65\ +\x4f\x6e\x55\x73\x65\x22\x20\x73\x70\x72\x65\x61\x64\x4d\x65\x74\ +\x68\x6f\x64\x3d\x22\x70\x61\x64\x22\x20\x69\x64\x3d\x22\x66\x69\ +\x6c\x6c\x31\x36\x22\x3e\x3c\x73\x74\x6f\x70\x20\x6f\x66\x66\x73\ +\x65\x74\x3d\x22\x30\x22\x20\x73\x74\x6f\x70\x2d\x63\x6f\x6c\x6f\ +\x72\x3d\x22\x23\x32\x30\x34\x42\x37\x38\x22\x2f\x3e\x3c\x73\x74\ +\x6f\x70\x20\x6f\x66\x66\x73\x65\x74\x3d\x22\x31\x22\x20\x73\x74\ +\x6f\x70\x2d\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x36\x31\x39\x39\x44\ +\x34\x22\x2f\x3e\x3c\x2f\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\ +\x69\x65\x6e\x74\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\x20\x63\ +\x6c\x69\x70\x2d\x70\x61\x74\x68\x3d\x22\x75\x72\x6c\x28\x23\x63\ +\x6c\x69\x70\x30\x29\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\ +\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x2d\x36\x31\x38\ +\x20\x2d\x32\x36\x36\x29\x22\x3e\x3c\x67\x20\x63\x6c\x69\x70\x2d\ +\x70\x61\x74\x68\x3d\x22\x75\x72\x6c\x28\x23\x63\x6c\x69\x70\x31\ +\x29\x22\x3e\x3c\x67\x20\x63\x6c\x69\x70\x2d\x70\x61\x74\x68\x3d\ +\x22\x75\x72\x6c\x28\x23\x63\x6c\x69\x70\x32\x29\x22\x3e\x3c\x67\ +\x20\x63\x6c\x69\x70\x2d\x70\x61\x74\x68\x3d\x22\x75\x72\x6c\x28\ +\x23\x63\x6c\x69\x70\x33\x29\x22\x3e\x3c\x67\x20\x63\x6c\x69\x70\ +\x2d\x70\x61\x74\x68\x3d\x22\x75\x72\x6c\x28\x23\x63\x6c\x69\x70\ +\x34\x29\x22\x3e\x3c\x67\x20\x63\x6c\x69\x70\x2d\x70\x61\x74\x68\ +\x3d\x22\x75\x72\x6c\x28\x23\x63\x6c\x69\x70\x35\x29\x22\x3e\x3c\ +\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x31\x31\x33\x2e\x33\x31\x39\ +\x20\x31\x33\x35\x2e\x37\x31\x33\x43\x31\x31\x33\x2e\x33\x31\x39\ +\x20\x31\x33\x35\x2e\x37\x31\x33\x20\x31\x31\x36\x2e\x36\x31\x31\ +\x20\x31\x34\x31\x2e\x39\x39\x38\x20\x31\x31\x32\x2e\x30\x36\x35\ +\x20\x31\x34\x37\x2e\x32\x37\x32\x20\x31\x30\x36\x2e\x33\x39\x37\ +\x20\x31\x35\x33\x2e\x38\x34\x38\x20\x39\x38\x2e\x38\x33\x32\x37\ +\x20\x31\x35\x30\x2e\x31\x39\x39\x20\x39\x38\x2e\x38\x33\x32\x37\ +\x20\x31\x35\x30\x2e\x31\x39\x39\x20\x39\x38\x2e\x38\x33\x32\x37\ +\x20\x31\x35\x30\x2e\x31\x39\x39\x20\x31\x30\x34\x2e\x39\x35\x34\ +\x20\x31\x34\x38\x2e\x38\x30\x31\x20\x31\x30\x38\x2e\x39\x34\x37\ +\x20\x31\x34\x34\x2e\x34\x35\x39\x20\x31\x31\x31\x2e\x38\x36\x32\ +\x20\x31\x34\x31\x2e\x32\x38\x39\x20\x31\x31\x33\x2e\x33\x31\x39\ +\x20\x31\x33\x35\x2e\x37\x31\x33\x20\x31\x31\x33\x2e\x33\x31\x39\ +\x20\x31\x33\x35\x2e\x37\x31\x33\x5a\x22\x20\x73\x74\x72\x6f\x6b\ +\x65\x3d\x22\x23\x31\x36\x31\x36\x31\x36\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x2d\x77\x69\x64\x74\x68\x3d\x22\x30\x2e\x30\x35\x33\x32\ +\x33\x32\x22\x20\x66\x69\x6c\x6c\x3d\x22\x75\x72\x6c\x28\x23\x66\ +\x69\x6c\x6c\x36\x29\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\ +\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x31\x20\x30\x20\x30\x20\x31\ +\x2e\x30\x31\x31\x37\x35\x20\x35\x33\x31\x2e\x34\x38\x32\x20\x31\ +\x34\x38\x2e\x37\x34\x35\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ +\x64\x3d\x22\x4d\x31\x30\x37\x2e\x31\x39\x38\x20\x31\x34\x34\x2e\ +\x36\x34\x38\x43\x31\x30\x37\x2e\x31\x39\x38\x20\x31\x34\x34\x2e\ +\x36\x34\x38\x20\x39\x37\x2e\x36\x30\x35\x33\x20\x31\x35\x33\x2e\ +\x30\x35\x36\x20\x38\x39\x2e\x38\x39\x37\x32\x20\x31\x34\x37\x2e\ +\x31\x39\x36\x20\x38\x35\x2e\x36\x37\x33\x35\x20\x31\x34\x33\x2e\ +\x39\x38\x35\x20\x39\x31\x2e\x37\x39\x38\x34\x20\x31\x33\x36\x2e\ +\x39\x33\x20\x39\x31\x2e\x37\x39\x38\x34\x20\x31\x33\x36\x2e\x39\ +\x33\x20\x39\x31\x2e\x37\x39\x38\x34\x20\x31\x33\x36\x2e\x39\x33\ +\x20\x38\x38\x2e\x35\x38\x33\x37\x20\x31\x34\x31\x2e\x37\x33\x38\ +\x20\x39\x33\x2e\x39\x32\x37\x37\x20\x31\x34\x34\x2e\x34\x39\x37\ +\x20\x39\x37\x2e\x38\x30\x31\x39\x20\x31\x34\x36\x2e\x34\x39\x37\ +\x20\x31\x30\x37\x2e\x31\x39\x38\x20\x31\x34\x34\x2e\x36\x34\x38\ +\x20\x31\x30\x37\x2e\x31\x39\x38\x20\x31\x34\x34\x2e\x36\x34\x38\ +\x5a\x22\x20\x73\x74\x72\x6f\x6b\x65\x3d\x22\x23\x34\x34\x34\x34\ +\x34\x34\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\ +\x3d\x22\x30\x2e\x30\x35\x33\x32\x33\x32\x22\x20\x66\x69\x6c\x6c\ +\x3d\x22\x75\x72\x6c\x28\x23\x66\x69\x6c\x6c\x37\x29\x22\x20\x74\ +\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\ +\x28\x31\x20\x30\x20\x30\x20\x31\x2e\x30\x31\x31\x37\x35\x20\x35\ +\x33\x31\x2e\x34\x38\x32\x20\x31\x34\x38\x2e\x37\x34\x35\x29\x22\ +\x2f\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x39\x36\x2e\x39\ +\x34\x30\x31\x20\x31\x32\x34\x2e\x35\x31\x38\x43\x39\x36\x2e\x39\ +\x34\x30\x31\x20\x31\x32\x34\x2e\x35\x31\x38\x20\x39\x35\x2e\x32\ +\x37\x38\x31\x20\x31\x32\x38\x2e\x36\x30\x34\x20\x39\x36\x2e\x36\ +\x37\x31\x32\x20\x31\x33\x35\x2e\x30\x35\x37\x20\x39\x37\x2e\x37\ +\x32\x39\x31\x20\x31\x33\x39\x2e\x39\x35\x36\x20\x31\x30\x32\x2e\ +\x31\x35\x36\x20\x31\x34\x33\x2e\x39\x33\x20\x31\x30\x32\x2e\x31\ +\x35\x36\x20\x31\x34\x33\x2e\x39\x33\x20\x31\x30\x32\x2e\x31\x35\ +\x36\x20\x31\x34\x33\x2e\x39\x33\x20\x39\x34\x2e\x32\x30\x35\x32\ +\x20\x31\x34\x33\x2e\x37\x36\x35\x20\x39\x32\x2e\x34\x32\x33\x32\ +\x20\x31\x33\x35\x2e\x35\x34\x31\x20\x39\x30\x2e\x39\x33\x39\x31\ +\x20\x31\x32\x38\x2e\x36\x39\x32\x20\x39\x36\x2e\x39\x34\x30\x31\ +\x20\x31\x32\x34\x2e\x35\x31\x38\x20\x39\x36\x2e\x39\x34\x30\x31\ +\x20\x31\x32\x34\x2e\x35\x31\x38\x5a\x22\x20\x73\x74\x72\x6f\x6b\ +\x65\x3d\x22\x23\x45\x33\x30\x45\x31\x46\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x2d\x77\x69\x64\x74\x68\x3d\x22\x30\x2e\x30\x35\x33\x32\ +\x33\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\ +\x74\x79\x3d\x22\x30\x2e\x39\x38\x34\x33\x31\x34\x22\x20\x66\x69\ +\x6c\x6c\x3d\x22\x75\x72\x6c\x28\x23\x66\x69\x6c\x6c\x38\x29\x22\ +\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\ +\x69\x78\x28\x31\x20\x30\x20\x30\x20\x31\x2e\x30\x31\x31\x37\x35\ +\x20\x35\x33\x31\x2e\x34\x38\x32\x20\x31\x34\x38\x2e\x37\x34\x35\ +\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x31\x30\ +\x30\x2e\x35\x34\x34\x20\x31\x33\x30\x2e\x35\x30\x34\x43\x31\x30\ +\x30\x2e\x35\x34\x34\x20\x31\x33\x30\x2e\x35\x30\x34\x20\x31\x30\ +\x34\x2e\x37\x20\x31\x32\x34\x2e\x32\x38\x33\x20\x31\x31\x32\x2e\ +\x30\x36\x35\x20\x31\x32\x35\x2e\x39\x38\x20\x31\x32\x30\x2e\x30\ +\x34\x39\x20\x31\x32\x37\x2e\x38\x31\x38\x20\x31\x32\x30\x2e\x32\ +\x30\x31\x20\x31\x33\x35\x2e\x35\x36\x31\x20\x31\x32\x30\x2e\x32\ +\x30\x31\x20\x31\x33\x35\x2e\x35\x36\x31\x20\x31\x32\x30\x2e\x32\ +\x30\x31\x20\x31\x33\x35\x2e\x35\x36\x31\x20\x31\x31\x34\x2e\x31\ +\x34\x37\x20\x31\x33\x30\x2e\x36\x31\x20\x31\x31\x30\x2e\x34\x36\ +\x38\x20\x31\x32\x39\x2e\x37\x34\x34\x20\x31\x30\x37\x2e\x32\x33\ +\x38\x20\x31\x32\x38\x2e\x39\x38\x34\x20\x31\x30\x30\x2e\x35\x34\ +\x34\x20\x31\x33\x30\x2e\x35\x30\x34\x20\x31\x30\x30\x2e\x35\x34\ +\x34\x20\x31\x33\x30\x2e\x35\x30\x34\x5a\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x3d\x22\x23\x43\x37\x33\x31\x34\x46\x22\x20\x73\x74\x72\ +\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\x3d\x22\x30\x2e\x30\x35\x33\ +\x32\x33\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\ +\x69\x74\x79\x3d\x22\x30\x2e\x39\x35\x32\x39\x34\x31\x22\x20\x66\ +\x69\x6c\x6c\x3d\x22\x75\x72\x6c\x28\x23\x66\x69\x6c\x6c\x39\x29\ +\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\ +\x72\x69\x78\x28\x31\x20\x30\x20\x30\x20\x31\x2e\x30\x31\x31\x37\ +\x35\x20\x35\x33\x31\x2e\x34\x38\x32\x20\x31\x34\x38\x2e\x37\x34\ +\x35\x29\x22\x2f\x3e\x3c\x67\x20\x63\x6c\x69\x70\x2d\x70\x61\x74\ +\x68\x3d\x22\x75\x72\x6c\x28\x23\x63\x6c\x69\x70\x31\x30\x29\x22\ +\x3e\x3c\x67\x20\x63\x6c\x69\x70\x2d\x70\x61\x74\x68\x3d\x22\x75\ +\x72\x6c\x28\x23\x63\x6c\x69\x70\x31\x31\x29\x22\x3e\x3c\x67\x20\ +\x63\x6c\x69\x70\x2d\x70\x61\x74\x68\x3d\x22\x75\x72\x6c\x28\x23\ +\x63\x6c\x69\x70\x31\x32\x29\x22\x3e\x3c\x67\x20\x63\x6c\x69\x70\ +\x2d\x70\x61\x74\x68\x3d\x22\x75\x72\x6c\x28\x23\x63\x6c\x69\x70\ +\x31\x33\x29\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ +\x6d\x61\x74\x72\x69\x78\x28\x30\x2e\x31\x34\x31\x31\x34\x32\x20\ +\x30\x20\x30\x20\x30\x2e\x31\x34\x32\x37\x37\x39\x20\x36\x32\x38\ +\x2e\x36\x31\x38\x20\x32\x36\x38\x2e\x30\x34\x36\x29\x22\x3e\x3c\ +\x67\x20\x63\x6c\x69\x70\x2d\x70\x61\x74\x68\x3d\x22\x75\x72\x6c\ +\x28\x23\x63\x6c\x69\x70\x31\x35\x29\x22\x20\x74\x72\x61\x6e\x73\ +\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x31\x20\x30\ +\x20\x30\x20\x31\x2e\x30\x30\x30\x37\x39\x20\x32\x2e\x39\x32\x31\ +\x35\x36\x65\x2d\x30\x35\x20\x34\x2e\x36\x31\x30\x38\x33\x65\x2d\ +\x30\x35\x29\x22\x3e\x3c\x75\x73\x65\x20\x77\x69\x64\x74\x68\x3d\ +\x22\x31\x30\x30\x25\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x31\ +\x30\x30\x25\x22\x20\x78\x6c\x69\x6e\x6b\x3a\x68\x72\x65\x66\x3d\ +\x22\x23\x69\x6d\x67\x31\x34\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\ +\x72\x6d\x3d\x22\x73\x63\x61\x6c\x65\x28\x31\x2e\x39\x38\x31\x34\ +\x38\x20\x31\x2e\x39\x38\x31\x34\x38\x29\x22\x3e\x3c\x2f\x75\x73\ +\x65\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\ +\x67\x3e\x3c\x2f\x67\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\ +\x31\x31\x31\x2e\x31\x36\x33\x20\x31\x33\x31\x2e\x32\x33\x39\x43\ +\x31\x31\x31\x2e\x31\x36\x33\x20\x31\x33\x31\x2e\x32\x33\x39\x20\ +\x31\x32\x32\x2e\x35\x35\x35\x20\x31\x33\x34\x2e\x38\x36\x35\x20\ +\x31\x32\x32\x2e\x33\x37\x34\x20\x31\x34\x33\x2e\x32\x35\x38\x20\ +\x31\x32\x32\x2e\x32\x20\x31\x35\x31\x2e\x33\x39\x35\x20\x31\x31\ +\x32\x2e\x31\x35\x38\x20\x31\x34\x38\x2e\x35\x20\x31\x31\x32\x2e\ +\x31\x35\x38\x20\x31\x34\x38\x2e\x35\x20\x31\x31\x32\x2e\x31\x35\ +\x38\x20\x31\x34\x38\x2e\x35\x20\x31\x31\x37\x2e\x36\x34\x37\x20\ +\x31\x34\x38\x2e\x38\x35\x20\x31\x31\x37\x2e\x38\x32\x36\x20\x31\ +\x34\x33\x2e\x33\x37\x20\x31\x31\x37\x2e\x39\x39\x33\x20\x31\x33\ +\x38\x2e\x32\x35\x35\x20\x31\x31\x31\x2e\x31\x36\x33\x20\x31\x33\ +\x31\x2e\x32\x33\x39\x20\x31\x31\x31\x2e\x31\x36\x33\x20\x31\x33\ +\x31\x2e\x32\x33\x39\x5a\x22\x20\x73\x74\x72\x6f\x6b\x65\x3d\x22\ +\x23\x33\x44\x36\x45\x41\x32\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\ +\x77\x69\x64\x74\x68\x3d\x22\x30\x2e\x30\x35\x33\x32\x33\x32\x22\ +\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\x3d\ +\x22\x30\x2e\x39\x38\x30\x33\x39\x32\x22\x20\x66\x69\x6c\x6c\x3d\ +\x22\x75\x72\x6c\x28\x23\x66\x69\x6c\x6c\x31\x36\x29\x22\x20\x74\ +\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\ +\x28\x31\x20\x30\x20\x30\x20\x31\x2e\x30\x31\x31\x37\x35\x20\x35\ +\x33\x31\x2e\x34\x38\x32\x20\x31\x34\x38\x2e\x37\x34\x35\x29\x22\ +\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x70\ +\x61\x74\x68\x20\x64\x3d\x22\x4d\x33\x30\x2e\x31\x30\x39\x36\x2d\ +\x32\x38\x2e\x30\x38\x37\x33\x20\x33\x30\x2e\x31\x30\x39\x36\x20\ +\x30\x2e\x31\x36\x38\x35\x32\x34\x20\x32\x32\x2e\x34\x31\x33\x37\ +\x20\x30\x2e\x31\x36\x38\x35\x32\x34\x20\x32\x32\x2e\x34\x31\x33\ +\x37\x2d\x32\x38\x2e\x30\x38\x37\x33\x5a\x4d\x31\x38\x2e\x33\x31\ +\x32\x39\x2d\x37\x2e\x30\x37\x38\x30\x31\x20\x32\x31\x2e\x30\x30\ +\x39\x33\x2d\x32\x2e\x31\x39\x30\x38\x31\x20\x32\x31\x2e\x30\x30\ +\x39\x33\x2d\x32\x2e\x31\x33\x34\x36\x34\x43\x31\x39\x2e\x30\x36\ +\x31\x39\x2d\x30\x2e\x34\x31\x31\x39\x34\x37\x20\x31\x36\x2e\x36\ +\x34\x36\x34\x20\x30\x2e\x34\x34\x39\x33\x39\x37\x20\x31\x33\x2e\ +\x37\x36\x32\x38\x20\x30\x2e\x34\x34\x39\x33\x39\x37\x20\x39\x2e\ +\x31\x39\x33\x39\x32\x20\x30\x2e\x34\x34\x39\x33\x39\x37\x20\x35\ +\x2e\x39\x35\x34\x35\x31\x2d\x30\x2e\x37\x38\x36\x34\x34\x35\x20\ +\x34\x2e\x30\x34\x34\x35\x37\x2d\x33\x2e\x32\x35\x38\x31\x33\x20\ +\x32\x2e\x32\x38\x34\x34\x34\x2d\x35\x2e\x34\x36\x37\x36\x37\x20\ +\x31\x2e\x34\x30\x34\x33\x37\x2d\x38\x2e\x39\x38\x37\x39\x34\x20\ +\x31\x2e\x34\x30\x34\x33\x37\x2d\x31\x33\x2e\x38\x31\x39\x4c\x31\ +\x2e\x34\x30\x34\x33\x37\x2d\x32\x38\x2e\x30\x38\x37\x33\x20\x39\ +\x2e\x31\x30\x30\x32\x39\x2d\x32\x38\x2e\x30\x38\x37\x33\x20\x39\ +\x2e\x31\x30\x30\x32\x39\x2d\x31\x33\x2e\x38\x31\x39\x43\x39\x2e\ +\x31\x30\x30\x32\x39\x2d\x31\x32\x2e\x38\x30\x37\x38\x20\x39\x2e\ +\x31\x31\x39\x30\x32\x2d\x31\x31\x2e\x38\x33\x34\x31\x20\x39\x2e\ +\x31\x35\x36\x34\x37\x2d\x31\x30\x2e\x38\x39\x37\x39\x20\x39\x2e\ +\x32\x33\x31\x33\x37\x2d\x39\x2e\x39\x36\x31\x36\x34\x20\x39\x2e\ +\x34\x33\x37\x33\x34\x2d\x39\x2e\x31\x33\x37\x37\x34\x20\x39\x2e\ +\x37\x37\x34\x33\x39\x2d\x38\x2e\x34\x32\x36\x32\x20\x31\x30\x2e\ +\x31\x34\x38\x39\x2d\x37\x2e\x37\x31\x34\x36\x35\x20\x31\x30\x2e\ +\x37\x31\x30\x36\x2d\x37\x2e\x31\x33\x34\x31\x38\x20\x31\x31\x2e\ +\x34\x35\x39\x36\x2d\x36\x2e\x36\x38\x34\x37\x38\x20\x31\x32\x2e\ +\x32\x30\x38\x36\x2d\x36\x2e\x32\x37\x32\x38\x34\x20\x31\x33\x2e\ +\x32\x37\x35\x39\x2d\x36\x2e\x30\x36\x36\x38\x36\x20\x31\x34\x2e\ +\x36\x36\x31\x36\x2d\x36\x2e\x30\x36\x36\x38\x36\x20\x31\x35\x2e\ +\x32\x39\x38\x32\x2d\x36\x2e\x30\x36\x36\x38\x36\x20\x31\x35\x2e\ +\x39\x31\x36\x32\x2d\x36\x2e\x31\x36\x30\x34\x39\x20\x31\x36\x2e\ +\x35\x31\x35\x33\x2d\x36\x2e\x33\x34\x37\x37\x33\x20\x31\x37\x2e\ +\x31\x31\x34\x35\x2d\x36\x2e\x35\x33\x34\x39\x38\x20\x31\x37\x2e\ +\x36\x37\x36\x33\x2d\x36\x2e\x37\x37\x38\x34\x31\x20\x31\x38\x2e\ +\x32\x30\x30\x36\x2d\x37\x2e\x30\x37\x38\x30\x31\x5a\x22\x20\x66\ +\x69\x6c\x6c\x3d\x22\x23\x42\x46\x42\x46\x42\x46\x22\x20\x74\x72\ +\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\ +\x31\x20\x30\x20\x30\x20\x31\x2e\x30\x31\x31\x36\x20\x35\x32\x36\ +\x2e\x33\x34\x37\x20\x33\x30\x31\x2e\x31\x37\x29\x22\x2f\x3e\x3c\ +\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x32\x32\x2e\x34\x36\x37\x35\ +\x2d\x32\x38\x2e\x30\x38\x37\x33\x20\x33\x30\x2e\x31\x30\x37\x32\ +\x2d\x32\x38\x2e\x30\x38\x37\x33\x20\x33\x30\x2e\x31\x30\x37\x32\ +\x20\x30\x20\x32\x32\x2e\x34\x36\x37\x35\x20\x30\x5a\x4d\x34\x38\ +\x2e\x34\x32\x30\x31\x2d\x32\x34\x2e\x38\x32\x39\x32\x43\x34\x39\ +\x2e\x33\x35\x36\x34\x2d\x32\x33\x2e\x36\x36\x38\x33\x20\x35\x30\ +\x2e\x30\x33\x30\x35\x2d\x32\x32\x2e\x32\x30\x37\x37\x20\x35\x30\ +\x2e\x34\x34\x32\x34\x2d\x32\x30\x2e\x34\x34\x37\x36\x20\x35\x30\ +\x2e\x38\x35\x34\x34\x2d\x31\x38\x2e\x36\x38\x37\x34\x20\x35\x31\ +\x2e\x30\x36\x30\x34\x2d\x31\x36\x2e\x35\x31\x35\x33\x20\x35\x31\ +\x2e\x30\x36\x30\x34\x2d\x31\x33\x2e\x39\x33\x31\x33\x4c\x35\x31\ +\x2e\x30\x36\x30\x34\x20\x30\x20\x34\x33\x2e\x34\x32\x30\x36\x20\ +\x30\x20\x34\x33\x2e\x34\x32\x30\x36\x2d\x31\x33\x2e\x39\x33\x31\ +\x33\x43\x34\x33\x2e\x34\x32\x30\x36\x2d\x31\x34\x2e\x39\x37\x39\ +\x39\x20\x34\x33\x2e\x33\x38\x33\x32\x2d\x31\x35\x2e\x39\x37\x32\ +\x33\x20\x34\x33\x2e\x33\x30\x38\x33\x2d\x31\x36\x2e\x39\x30\x38\ +\x36\x20\x34\x33\x2e\x32\x37\x30\x38\x2d\x31\x37\x2e\x38\x38\x32\ +\x33\x20\x34\x33\x2e\x30\x38\x33\x36\x2d\x31\x38\x2e\x37\x32\x34\ +\x39\x20\x34\x32\x2e\x37\x34\x36\x35\x2d\x31\x39\x2e\x34\x33\x36\ +\x34\x20\x34\x32\x2e\x34\x30\x39\x35\x2d\x32\x30\x2e\x31\x38\x35\ +\x34\x20\x34\x31\x2e\x38\x36\x36\x34\x2d\x32\x30\x2e\x37\x36\x35\ +\x39\x20\x34\x31\x2e\x31\x31\x37\x34\x2d\x32\x31\x2e\x31\x37\x37\ +\x38\x20\x34\x30\x2e\x33\x36\x38\x34\x2d\x32\x31\x2e\x35\x38\x39\ +\x38\x20\x33\x39\x2e\x33\x30\x31\x31\x2d\x32\x31\x2e\x37\x39\x35\ +\x38\x20\x33\x37\x2e\x39\x31\x35\x35\x2d\x32\x31\x2e\x37\x39\x35\ +\x38\x20\x33\x36\x2e\x36\x30\x34\x37\x2d\x32\x31\x2e\x37\x39\x35\ +\x38\x20\x33\x35\x2e\x33\x38\x37\x36\x2d\x32\x31\x2e\x35\x31\x34\ +\x39\x20\x33\x34\x2e\x32\x36\x34\x31\x2d\x32\x30\x2e\x39\x35\x33\ +\x31\x4c\x33\x34\x2e\x32\x36\x34\x31\x2d\x32\x30\x2e\x38\x39\x37\ +\x20\x33\x34\x2e\x32\x30\x38\x2d\x32\x30\x2e\x39\x35\x33\x31\x20\ +\x33\x31\x2e\x34\x35\x35\x34\x2d\x32\x35\x2e\x38\x34\x30\x33\x20\ +\x33\x31\x2e\x35\x31\x31\x36\x2d\x32\x35\x2e\x38\x39\x36\x35\x43\ +\x33\x33\x2e\x34\x35\x39\x2d\x32\x37\x2e\x36\x35\x36\x37\x20\x33\ +\x35\x2e\x38\x39\x33\x32\x2d\x32\x38\x2e\x35\x33\x36\x37\x20\x33\ +\x38\x2e\x38\x31\x34\x33\x2d\x32\x38\x2e\x35\x33\x36\x37\x20\x34\ +\x33\x2e\x33\x30\x38\x33\x2d\x32\x38\x2e\x35\x33\x36\x37\x20\x34\ +\x36\x2e\x35\x31\x30\x32\x2d\x32\x37\x2e\x33\x30\x30\x39\x20\x34\ +\x38\x2e\x34\x32\x30\x31\x2d\x32\x34\x2e\x38\x32\x39\x32\x5a\x22\ +\x20\x66\x69\x6c\x6c\x3d\x22\x23\x42\x46\x42\x46\x42\x46\x22\x20\ +\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\ +\x78\x28\x31\x20\x30\x20\x30\x20\x31\x2e\x30\x31\x31\x36\x20\x35\ +\x32\x36\x2e\x33\x34\x37\x20\x33\x30\x31\x2e\x31\x37\x29\x22\x2f\ +\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x35\x34\x2e\x31\x34\ +\x39\x39\x20\x30\x20\x35\x34\x2e\x31\x34\x39\x39\x2d\x32\x38\x2e\ +\x30\x38\x37\x33\x20\x36\x31\x2e\x36\x32\x31\x31\x2d\x32\x38\x2e\ +\x30\x38\x37\x33\x20\x36\x31\x2e\x36\x32\x31\x31\x20\x30\x5a\x4d\ +\x35\x34\x2e\x31\x34\x39\x39\x2d\x33\x39\x2e\x33\x32\x32\x33\x20\ +\x36\x31\x2e\x36\x32\x31\x31\x2d\x33\x39\x2e\x33\x32\x32\x33\x20\ +\x36\x31\x2e\x36\x32\x31\x31\x2d\x33\x32\x2e\x37\x34\x39\x38\x20\ +\x35\x34\x2e\x31\x34\x39\x39\x2d\x33\x32\x2e\x37\x34\x39\x38\x5a\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x42\x46\x42\x46\x42\x46\x22\ +\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\ +\x69\x78\x28\x31\x20\x30\x20\x30\x20\x31\x2e\x30\x31\x31\x36\x20\ +\x35\x32\x36\x2e\x33\x34\x37\x20\x33\x30\x31\x2e\x31\x37\x29\x22\ +\x2f\x3e\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x4d\x38\x35\x2e\x34\ +\x39\x35\x33\x2d\x31\x30\x2e\x37\x32\x39\x34\x20\x39\x31\x2e\x35\ +\x36\x32\x32\x2d\x36\x2e\x37\x34\x30\x39\x36\x20\x39\x31\x2e\x35\ +\x30\x36\x2d\x36\x2e\x36\x38\x34\x37\x38\x43\x39\x30\x2e\x31\x39\ +\x35\x32\x2d\x34\x2e\x34\x33\x37\x38\x20\x38\x38\x2e\x34\x31\x36\ +\x34\x2d\x32\x2e\x36\x37\x37\x36\x36\x20\x38\x36\x2e\x31\x36\x39\ +\x34\x2d\x31\x2e\x34\x30\x34\x33\x37\x20\x38\x33\x2e\x39\x35\x39\ +\x39\x2d\x30\x2e\x31\x33\x31\x30\x37\x34\x20\x38\x31\x2e\x35\x36\ +\x33\x31\x20\x30\x2e\x35\x30\x35\x35\x37\x32\x20\x37\x38\x2e\x39\ +\x37\x39\x20\x30\x2e\x35\x30\x35\x35\x37\x32\x20\x37\x36\x2e\x39\ +\x35\x36\x37\x20\x30\x2e\x35\x30\x35\x35\x37\x32\x20\x37\x35\x2e\ +\x30\x36\x35\x35\x20\x30\x2e\x31\x33\x31\x30\x37\x34\x20\x37\x33\ +\x2e\x33\x30\x35\x34\x2d\x30\x2e\x36\x31\x37\x39\x32\x31\x20\x37\ +\x31\x2e\x35\x34\x35\x33\x2d\x31\x2e\x34\x30\x34\x33\x37\x20\x37\ +\x30\x2e\x30\x30\x39\x38\x2d\x32\x2e\x34\x35\x32\x39\x36\x20\x36\ +\x38\x2e\x36\x39\x39\x31\x2d\x33\x2e\x37\x36\x33\x37\x20\x36\x37\ +\x2e\x33\x38\x38\x33\x2d\x35\x2e\x30\x37\x34\x34\x34\x20\x36\x36\ +\x2e\x33\x33\x39\x37\x2d\x36\x2e\x36\x30\x39\x38\x38\x20\x36\x35\ +\x2e\x35\x35\x33\x33\x2d\x38\x2e\x33\x37\x30\x30\x32\x20\x36\x34\ +\x2e\x38\x30\x34\x33\x2d\x31\x30\x2e\x31\x33\x30\x32\x20\x36\x34\ +\x2e\x34\x32\x39\x38\x2d\x31\x32\x2e\x30\x30\x32\x36\x20\x36\x34\ +\x2e\x34\x32\x39\x38\x2d\x31\x33\x2e\x39\x38\x37\x35\x20\x36\x34\ +\x2e\x34\x32\x39\x38\x2d\x31\x35\x2e\x39\x37\x32\x33\x20\x36\x34\ +\x2e\x38\x30\x34\x33\x2d\x31\x37\x2e\x38\x34\x34\x38\x20\x36\x35\ +\x2e\x35\x35\x33\x33\x2d\x31\x39\x2e\x36\x30\x34\x39\x20\x36\x36\ +\x2e\x33\x33\x39\x37\x2d\x32\x31\x2e\x33\x36\x35\x31\x20\x36\x37\ +\x2e\x33\x38\x38\x33\x2d\x32\x32\x2e\x39\x30\x30\x35\x20\x36\x38\ +\x2e\x36\x39\x39\x31\x2d\x32\x34\x2e\x32\x31\x31\x33\x20\x37\x30\ +\x2e\x30\x30\x39\x38\x2d\x32\x35\x2e\x35\x32\x32\x20\x37\x31\x2e\ +\x35\x34\x35\x33\x2d\x32\x36\x2e\x35\x35\x31\x39\x20\x37\x33\x2e\ +\x33\x30\x35\x34\x2d\x32\x37\x2e\x33\x30\x30\x39\x20\x37\x35\x2e\ +\x30\x36\x35\x35\x2d\x32\x38\x2e\x30\x38\x37\x33\x20\x37\x36\x2e\ +\x39\x35\x36\x37\x2d\x32\x38\x2e\x34\x38\x30\x35\x20\x37\x38\x2e\ +\x39\x37\x39\x2d\x32\x38\x2e\x34\x38\x30\x35\x20\x38\x31\x2e\x35\ +\x36\x33\x31\x2d\x32\x38\x2e\x34\x38\x30\x35\x20\x38\x33\x2e\x39\ +\x35\x39\x39\x2d\x32\x37\x2e\x38\x34\x33\x39\x20\x38\x36\x2e\x31\ +\x36\x39\x34\x2d\x32\x36\x2e\x35\x37\x30\x36\x20\x38\x38\x2e\x34\ +\x31\x36\x34\x2d\x32\x35\x2e\x32\x39\x37\x33\x20\x39\x30\x2e\x31\ +\x39\x35\x32\x2d\x32\x33\x2e\x35\x33\x37\x32\x20\x39\x31\x2e\x35\ +\x30\x36\x2d\x32\x31\x2e\x32\x39\x30\x32\x4c\x39\x31\x2e\x35\x36\ +\x32\x32\x2d\x32\x31\x2e\x32\x33\x34\x20\x38\x35\x2e\x34\x33\x39\ +\x31\x2d\x31\x37\x2e\x32\x34\x35\x36\x20\x38\x35\x2e\x33\x38\x32\ +\x39\x2d\x31\x37\x2e\x33\x30\x31\x38\x43\x38\x34\x2e\x37\x38\x33\ +\x38\x2d\x31\x38\x2e\x36\x38\x37\x34\x20\x38\x33\x2e\x39\x30\x33\ +\x37\x2d\x31\x39\x2e\x38\x31\x30\x39\x20\x38\x32\x2e\x37\x34\x32\ +\x37\x2d\x32\x30\x2e\x36\x37\x32\x33\x20\x38\x31\x2e\x36\x31\x39\ +\x32\x2d\x32\x31\x2e\x35\x33\x33\x36\x20\x38\x30\x2e\x33\x36\x34\ +\x37\x2d\x32\x31\x2e\x39\x36\x34\x33\x20\x37\x38\x2e\x39\x37\x39\ +\x2d\x32\x31\x2e\x39\x36\x34\x33\x20\x37\x37\x2e\x39\x36\x37\x39\ +\x2d\x32\x31\x2e\x39\x36\x34\x33\x20\x37\x37\x2e\x30\x31\x32\x39\ +\x2d\x32\x31\x2e\x37\x33\x39\x36\x20\x37\x36\x2e\x31\x31\x34\x31\ +\x2d\x32\x31\x2e\x32\x39\x30\x32\x20\x37\x35\x2e\x32\x35\x32\x38\ +\x2d\x32\x30\x2e\x38\x37\x38\x32\x20\x37\x34\x2e\x35\x30\x33\x38\ +\x2d\x32\x30\x2e\x32\x39\x37\x38\x20\x37\x33\x2e\x38\x36\x37\x31\ +\x2d\x31\x39\x2e\x35\x34\x38\x38\x20\x37\x33\x2e\x32\x33\x30\x35\ +\x2d\x31\x38\x2e\x38\x33\x37\x32\x20\x37\x32\x2e\x37\x32\x34\x39\ +\x2d\x31\x37\x2e\x39\x39\x34\x36\x20\x37\x32\x2e\x33\x35\x30\x34\ +\x2d\x31\x37\x2e\x30\x32\x30\x39\x20\x37\x31\x2e\x39\x37\x35\x39\ +\x2d\x31\x36\x2e\x30\x38\x34\x37\x20\x37\x31\x2e\x37\x38\x38\x37\ +\x2d\x31\x35\x2e\x30\x37\x33\x35\x20\x37\x31\x2e\x37\x38\x38\x37\ +\x2d\x31\x33\x2e\x39\x38\x37\x35\x20\x37\x31\x2e\x37\x38\x38\x37\ +\x2d\x31\x32\x2e\x39\x30\x31\x34\x20\x37\x31\x2e\x39\x37\x35\x39\ +\x2d\x31\x31\x2e\x38\x39\x30\x33\x20\x37\x32\x2e\x33\x35\x30\x34\ +\x2d\x31\x30\x2e\x39\x35\x34\x31\x20\x37\x32\x2e\x37\x32\x34\x39\ +\x2d\x31\x30\x2e\x30\x31\x37\x38\x20\x37\x33\x2e\x32\x33\x30\x35\ +\x2d\x39\x2e\x31\x39\x33\x39\x32\x20\x37\x33\x2e\x38\x36\x37\x31\ +\x2d\x38\x2e\x34\x38\x32\x33\x37\x20\x37\x34\x2e\x35\x30\x33\x38\ +\x2d\x37\x2e\x38\x30\x38\x32\x38\x20\x37\x35\x2e\x32\x35\x32\x38\ +\x2d\x37\x2e\x32\x36\x35\x32\x35\x20\x37\x36\x2e\x31\x31\x34\x31\ +\x2d\x36\x2e\x38\x35\x33\x33\x31\x20\x37\x37\x2e\x30\x31\x32\x39\ +\x2d\x36\x2e\x34\x37\x38\x38\x31\x20\x37\x37\x2e\x39\x36\x37\x39\ +\x2d\x36\x2e\x32\x39\x31\x35\x36\x20\x37\x38\x2e\x39\x37\x39\x2d\ +\x36\x2e\x32\x39\x31\x35\x36\x20\x38\x30\x2e\x33\x36\x34\x37\x2d\ +\x36\x2e\x32\x39\x31\x35\x36\x20\x38\x31\x2e\x36\x31\x39\x32\x2d\ +\x36\x2e\x36\x36\x36\x30\x36\x20\x38\x32\x2e\x37\x34\x32\x37\x2d\ +\x37\x2e\x34\x31\x35\x30\x35\x20\x38\x33\x2e\x39\x30\x33\x37\x2d\ +\x38\x2e\x32\x30\x31\x35\x20\x38\x34\x2e\x37\x38\x33\x38\x2d\x39\ +\x2e\x32\x38\x37\x35\x34\x20\x38\x35\x2e\x33\x38\x32\x39\x2d\x31\ +\x30\x2e\x36\x37\x33\x32\x4c\x38\x35\x2e\x34\x33\x39\x31\x2d\x31\ +\x30\x2e\x37\x38\x35\x35\x5a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\ +\x42\x46\x42\x46\x42\x46\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\ +\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x31\x20\x30\x20\x30\x20\ +\x31\x2e\x30\x31\x31\x36\x20\x35\x32\x36\x2e\x33\x34\x37\x20\x33\ +\x30\x31\x2e\x31\x37\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x64\ +\x3d\x22\x4d\x31\x36\x30\x2e\x30\x38\x34\x2d\x33\x39\x2e\x33\x32\ +\x32\x33\x20\x31\x36\x30\x2e\x30\x38\x34\x20\x30\x20\x31\x35\x32\ +\x2e\x36\x31\x33\x20\x30\x20\x31\x35\x32\x2e\x36\x36\x39\x2d\x31\ +\x34\x2e\x30\x39\x39\x38\x43\x31\x35\x32\x2e\x36\x36\x39\x2d\x31\ +\x35\x2e\x31\x34\x38\x34\x20\x31\x35\x32\x2e\x34\x34\x34\x2d\x31\ +\x36\x2e\x31\x34\x30\x38\x20\x31\x35\x31\x2e\x39\x39\x35\x2d\x31\ +\x37\x2e\x30\x37\x37\x31\x20\x31\x35\x31\x2e\x35\x38\x33\x2d\x31\ +\x38\x2e\x30\x31\x33\x33\x20\x31\x35\x31\x2e\x30\x32\x31\x2d\x31\ +\x38\x2e\x38\x31\x38\x35\x20\x31\x35\x30\x2e\x33\x30\x39\x2d\x31\ +\x39\x2e\x34\x39\x32\x36\x20\x31\x34\x39\x2e\x36\x33\x35\x2d\x32\ +\x30\x2e\x32\x30\x34\x31\x20\x31\x34\x38\x2e\x38\x31\x31\x2d\x32\ +\x30\x2e\x37\x34\x37\x32\x20\x31\x34\x37\x2e\x38\x33\x38\x2d\x32\ +\x31\x2e\x31\x32\x31\x37\x20\x31\x34\x36\x2e\x39\x30\x31\x2d\x32\ +\x31\x2e\x35\x33\x33\x36\x20\x31\x34\x35\x2e\x38\x39\x2d\x32\x31\ +\x2e\x37\x33\x39\x36\x20\x31\x34\x34\x2e\x38\x30\x34\x2d\x32\x31\ +\x2e\x37\x33\x39\x36\x20\x31\x34\x33\x2e\x37\x31\x38\x2d\x32\x31\ +\x2e\x37\x33\x39\x36\x20\x31\x34\x32\x2e\x37\x30\x37\x2d\x32\x31\ +\x2e\x35\x33\x33\x36\x20\x31\x34\x31\x2e\x37\x37\x31\x2d\x32\x31\ +\x2e\x31\x32\x31\x37\x20\x31\x34\x30\x2e\x38\x33\x35\x2d\x32\x30\ +\x2e\x37\x34\x37\x32\x20\x31\x34\x30\x2e\x30\x31\x31\x2d\x32\x30\ +\x2e\x32\x30\x34\x31\x20\x31\x33\x39\x2e\x32\x39\x39\x2d\x31\x39\ +\x2e\x34\x39\x32\x36\x20\x31\x33\x38\x2e\x36\x32\x35\x2d\x31\x38\ +\x2e\x37\x38\x31\x31\x20\x31\x33\x38\x2e\x30\x38\x32\x2d\x31\x37\ +\x2e\x39\x35\x37\x32\x20\x31\x33\x37\x2e\x36\x37\x2d\x31\x37\x2e\ +\x30\x32\x30\x39\x20\x31\x33\x37\x2e\x32\x35\x38\x2d\x31\x36\x2e\ +\x30\x38\x34\x37\x20\x31\x33\x37\x2e\x30\x35\x32\x2d\x31\x35\x2e\ +\x30\x37\x33\x35\x20\x31\x33\x37\x2e\x30\x35\x32\x2d\x31\x33\x2e\ +\x39\x38\x37\x35\x20\x31\x33\x37\x2e\x30\x35\x32\x2d\x31\x32\x2e\ +\x39\x30\x31\x34\x20\x31\x33\x37\x2e\x32\x35\x38\x2d\x31\x31\x2e\ +\x38\x39\x30\x33\x20\x31\x33\x37\x2e\x36\x37\x2d\x31\x30\x2e\x39\ +\x35\x34\x31\x20\x31\x33\x38\x2e\x30\x38\x32\x2d\x31\x30\x2e\x30\ +\x31\x37\x38\x20\x31\x33\x38\x2e\x36\x32\x35\x2d\x39\x2e\x31\x39\ +\x33\x39\x32\x20\x31\x33\x39\x2e\x32\x39\x39\x2d\x38\x2e\x34\x38\ +\x32\x33\x37\x20\x31\x34\x30\x2e\x30\x31\x31\x2d\x37\x2e\x38\x30\ +\x38\x32\x38\x20\x31\x34\x30\x2e\x38\x33\x35\x2d\x37\x2e\x32\x36\ +\x35\x32\x35\x20\x31\x34\x31\x2e\x37\x37\x31\x2d\x36\x2e\x38\x35\ +\x33\x33\x31\x20\x31\x34\x32\x2e\x37\x30\x37\x2d\x36\x2e\x34\x34\ +\x31\x33\x36\x20\x31\x34\x33\x2e\x37\x31\x38\x2d\x36\x2e\x32\x33\ +\x35\x33\x39\x20\x31\x34\x34\x2e\x38\x30\x34\x2d\x36\x2e\x32\x33\ +\x35\x33\x39\x20\x31\x34\x35\x2e\x33\x32\x39\x2d\x36\x2e\x32\x33\ +\x35\x33\x39\x20\x31\x34\x35\x2e\x38\x35\x33\x2d\x36\x2e\x32\x37\ +\x32\x38\x34\x20\x31\x34\x36\x2e\x33\x37\x37\x2d\x36\x2e\x33\x34\ +\x37\x37\x33\x20\x31\x34\x36\x2e\x39\x30\x31\x2d\x36\x2e\x34\x32\ +\x32\x36\x33\x20\x31\x34\x37\x2e\x33\x38\x38\x2d\x36\x2e\x35\x37\ +\x32\x34\x33\x20\x31\x34\x37\x2e\x38\x33\x38\x2d\x36\x2e\x37\x39\ +\x37\x31\x33\x4c\x31\x34\x37\x2e\x38\x39\x34\x2d\x36\x2e\x37\x39\ +\x37\x31\x33\x20\x31\x34\x37\x2e\x39\x35\x2d\x36\x2e\x37\x39\x37\ +\x31\x33\x20\x31\x35\x30\x2e\x37\x30\x33\x2d\x31\x2e\x37\x34\x31\ +\x34\x31\x20\x31\x35\x30\x2e\x36\x34\x36\x2d\x31\x2e\x36\x38\x35\ +\x32\x34\x43\x31\x34\x38\x2e\x38\x31\x31\x2d\x30\x2e\x32\x36\x32\ +\x31\x34\x38\x20\x31\x34\x36\x2e\x35\x36\x34\x20\x30\x2e\x34\x34\ +\x39\x33\x39\x37\x20\x31\x34\x33\x2e\x39\x30\x35\x20\x30\x2e\x34\ +\x34\x39\x33\x39\x37\x20\x31\x34\x31\x2e\x38\x38\x33\x20\x30\x2e\ +\x34\x34\x39\x33\x39\x37\x20\x31\x33\x39\x2e\x39\x39\x32\x20\x30\ +\x2e\x30\x37\x34\x38\x39\x39\x35\x20\x31\x33\x38\x2e\x32\x33\x32\ +\x2d\x30\x2e\x36\x37\x34\x30\x39\x36\x20\x31\x33\x36\x2e\x34\x37\ +\x32\x2d\x31\x2e\x34\x32\x33\x30\x39\x20\x31\x33\x34\x2e\x39\x33\ +\x36\x2d\x32\x2e\x34\x35\x32\x39\x36\x20\x31\x33\x33\x2e\x36\x32\ +\x36\x2d\x33\x2e\x37\x36\x33\x37\x20\x31\x33\x32\x2e\x33\x31\x35\ +\x2d\x35\x2e\x30\x37\x34\x34\x34\x20\x31\x33\x31\x2e\x32\x36\x36\ +\x2d\x36\x2e\x36\x30\x39\x38\x38\x20\x31\x33\x30\x2e\x34\x38\x2d\ +\x38\x2e\x33\x37\x30\x30\x32\x20\x31\x32\x39\x2e\x37\x33\x31\x2d\ +\x31\x30\x2e\x31\x33\x30\x32\x20\x31\x32\x39\x2e\x33\x35\x36\x2d\ +\x31\x32\x2e\x30\x30\x32\x36\x20\x31\x32\x39\x2e\x33\x35\x36\x2d\ +\x31\x33\x2e\x39\x38\x37\x35\x20\x31\x32\x39\x2e\x33\x35\x36\x2d\ +\x31\x35\x2e\x39\x37\x32\x33\x20\x31\x32\x39\x2e\x37\x33\x31\x2d\ +\x31\x37\x2e\x38\x34\x34\x38\x20\x31\x33\x30\x2e\x34\x38\x2d\x31\ +\x39\x2e\x36\x30\x34\x39\x20\x31\x33\x31\x2e\x32\x36\x36\x2d\x32\ +\x31\x2e\x33\x36\x35\x31\x20\x31\x33\x32\x2e\x33\x31\x35\x2d\x32\ +\x32\x2e\x39\x30\x30\x35\x20\x31\x33\x33\x2e\x36\x32\x36\x2d\x32\ +\x34\x2e\x32\x31\x31\x33\x20\x31\x33\x34\x2e\x39\x33\x36\x2d\x32\ +\x35\x2e\x35\x32\x32\x20\x31\x33\x36\x2e\x34\x37\x32\x2d\x32\x36\ +\x2e\x35\x35\x31\x39\x20\x31\x33\x38\x2e\x32\x33\x32\x2d\x32\x37\ +\x2e\x33\x30\x30\x39\x20\x31\x33\x39\x2e\x39\x39\x32\x2d\x32\x38\ +\x2e\x30\x34\x39\x39\x20\x31\x34\x31\x2e\x38\x38\x33\x2d\x32\x38\ +\x2e\x34\x32\x34\x34\x20\x31\x34\x33\x2e\x39\x30\x35\x2d\x32\x38\ +\x2e\x34\x32\x34\x34\x20\x31\x34\x37\x2e\x37\x32\x35\x2d\x32\x38\ +\x2e\x34\x32\x34\x34\x20\x31\x35\x30\x2e\x36\x34\x36\x2d\x32\x37\ +\x2e\x30\x30\x31\x33\x20\x31\x35\x32\x2e\x36\x36\x39\x2d\x32\x34\ +\x2e\x31\x35\x35\x31\x4c\x31\x35\x32\x2e\x36\x36\x39\x2d\x33\x39\ +\x2e\x33\x32\x32\x33\x5a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x42\ +\x46\x42\x46\x42\x46\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\ +\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x31\x20\x30\x20\x30\x20\x31\ +\x2e\x30\x31\x31\x36\x20\x35\x32\x36\x2e\x33\x34\x37\x20\x33\x30\ +\x31\x2e\x31\x37\x29\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\ +\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x21\x27\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x77\x69\x64\x74\x68\ +\x3d\x22\x32\x35\x6d\x6d\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\ +\x31\x30\x38\x6d\x6d\x22\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\ +\x31\x2e\x31\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\ +\x30\x20\x32\x35\x20\x31\x30\x38\x22\x20\x78\x6d\x6c\x6e\x73\x3d\ +\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\ +\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x78\x6d\x6c\ +\x6e\x73\x3a\x63\x63\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x63\x72\ +\x65\x61\x74\x69\x76\x65\x63\x6f\x6d\x6d\x6f\x6e\x73\x2e\x6f\x72\ +\x67\x2f\x6e\x73\x23\x22\x20\x78\x6d\x6c\x6e\x73\x3a\x64\x63\x3d\ +\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\x2e\x6f\x72\x67\ +\x2f\x64\x63\x2f\x65\x6c\x65\x6d\x65\x6e\x74\x73\x2f\x31\x2e\x31\ +\x2f\x22\x20\x78\x6d\x6c\x6e\x73\x3a\x72\x64\x66\x3d\x22\x68\x74\ +\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\ +\x31\x39\x39\x39\x2f\x30\x32\x2f\x32\x32\x2d\x72\x64\x66\x2d\x73\ +\x79\x6e\x74\x61\x78\x2d\x6e\x73\x23\x22\x20\x78\x6d\x6c\x6e\x73\ +\x3a\x78\x6c\x69\x6e\x6b\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\ +\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x31\x39\x39\x39\x2f\x78\ +\x6c\x69\x6e\x6b\x22\x3e\x0a\x20\x3c\x64\x65\x66\x73\x3e\x0a\x20\ +\x20\x3c\x63\x6c\x69\x70\x50\x61\x74\x68\x3e\x0a\x20\x20\x20\x3c\ +\x72\x65\x63\x74\x20\x78\x3d\x22\x2d\x31\x34\x31\x22\x20\x79\x3d\ +\x22\x2d\x38\x32\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x33\x30\ +\x30\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x36\x36\x39\x22\x2f\ +\x3e\x0a\x20\x20\x3c\x2f\x63\x6c\x69\x70\x50\x61\x74\x68\x3e\x0a\ +\x20\x20\x3c\x63\x6c\x69\x70\x50\x61\x74\x68\x20\x69\x64\x3d\x22\ +\x63\x6c\x69\x70\x30\x2d\x33\x22\x3e\x0a\x20\x20\x20\x3c\x72\x65\ +\x63\x74\x20\x78\x3d\x22\x2d\x31\x34\x31\x22\x20\x79\x3d\x22\x2d\ +\x38\x32\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x33\x30\x30\x22\ +\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x36\x36\x39\x22\x2f\x3e\x0a\ +\x20\x20\x3c\x2f\x63\x6c\x69\x70\x50\x61\x74\x68\x3e\x0a\x20\x20\ +\x3c\x63\x6c\x69\x70\x50\x61\x74\x68\x20\x69\x64\x3d\x22\x63\x6c\ +\x69\x70\x31\x35\x2d\x30\x39\x22\x3e\x0a\x20\x20\x20\x3c\x72\x65\ +\x63\x74\x20\x78\x3d\x22\x39\x37\x33\x22\x20\x79\x3d\x22\x31\x36\ +\x34\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x35\x38\x22\x20\x68\ +\x65\x69\x67\x68\x74\x3d\x22\x31\x35\x38\x22\x2f\x3e\x0a\x20\x20\ +\x3c\x2f\x63\x6c\x69\x70\x50\x61\x74\x68\x3e\x0a\x20\x20\x3c\x63\ +\x6c\x69\x70\x50\x61\x74\x68\x20\x69\x64\x3d\x22\x63\x6c\x69\x70\ +\x31\x36\x2d\x31\x22\x3e\x0a\x20\x20\x20\x3c\x72\x65\x63\x74\x20\ +\x78\x3d\x22\x39\x37\x33\x22\x20\x79\x3d\x22\x31\x36\x34\x22\x20\ +\x77\x69\x64\x74\x68\x3d\x22\x31\x35\x38\x22\x20\x68\x65\x69\x67\ +\x68\x74\x3d\x22\x31\x35\x38\x22\x2f\x3e\x0a\x20\x20\x3c\x2f\x63\ +\x6c\x69\x70\x50\x61\x74\x68\x3e\x0a\x20\x20\x3c\x63\x6c\x69\x70\ +\x50\x61\x74\x68\x20\x69\x64\x3d\x22\x63\x6c\x69\x70\x31\x37\x2d\ +\x38\x39\x22\x3e\x0a\x20\x20\x20\x3c\x72\x65\x63\x74\x20\x78\x3d\ +\x22\x39\x37\x33\x22\x20\x79\x3d\x22\x31\x36\x34\x22\x20\x77\x69\ +\x64\x74\x68\x3d\x22\x31\x35\x38\x22\x20\x68\x65\x69\x67\x68\x74\ +\x3d\x22\x31\x35\x38\x22\x2f\x3e\x0a\x20\x20\x3c\x2f\x63\x6c\x69\ +\x70\x50\x61\x74\x68\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x61\x72\ +\x47\x72\x61\x64\x69\x65\x6e\x74\x20\x69\x64\x3d\x22\x6c\x69\x6e\ +\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x33\x32\x38\x34\x22\ +\x20\x78\x31\x3d\x22\x31\x38\x33\x2e\x35\x36\x22\x20\x78\x32\x3d\ +\x22\x31\x32\x37\x2e\x36\x32\x22\x20\x79\x31\x3d\x22\x31\x36\x31\ +\x2e\x30\x38\x22\x20\x79\x32\x3d\x22\x31\x30\x30\x2e\x32\x33\x22\ +\x20\x67\x72\x61\x64\x69\x65\x6e\x74\x54\x72\x61\x6e\x73\x66\x6f\ +\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x34\x32\ +\x34\x2e\x39\x20\x35\x35\x32\x2e\x30\x38\x29\x22\x20\x67\x72\x61\ +\x64\x69\x65\x6e\x74\x55\x6e\x69\x74\x73\x3d\x22\x75\x73\x65\x72\ +\x53\x70\x61\x63\x65\x4f\x6e\x55\x73\x65\x22\x3e\x0a\x20\x20\x20\ +\x3c\x73\x74\x6f\x70\x20\x73\x74\x6f\x70\x2d\x63\x6f\x6c\x6f\x72\ +\x3d\x22\x23\x33\x30\x37\x30\x62\x33\x22\x20\x6f\x66\x66\x73\x65\ +\x74\x3d\x22\x30\x22\x2f\x3e\x0a\x20\x20\x20\x3c\x73\x74\x6f\x70\ +\x20\x73\x74\x6f\x70\x2d\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x38\x36\ +\x62\x31\x64\x66\x22\x20\x6f\x66\x66\x73\x65\x74\x3d\x22\x31\x22\ +\x2f\x3e\x0a\x20\x20\x3c\x2f\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\ +\x64\x69\x65\x6e\x74\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x61\x72\ +\x47\x72\x61\x64\x69\x65\x6e\x74\x20\x69\x64\x3d\x22\x6c\x69\x6e\ +\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x31\x31\x38\x39\x2d\ +\x30\x2d\x36\x2d\x32\x22\x20\x78\x31\x3d\x22\x31\x37\x32\x2e\x35\ +\x35\x22\x20\x78\x32\x3d\x22\x31\x30\x34\x2e\x35\x31\x22\x20\x79\ +\x31\x3d\x22\x31\x32\x31\x2e\x37\x31\x22\x20\x79\x32\x3d\x22\x38\ +\x35\x2e\x36\x31\x35\x22\x20\x67\x72\x61\x64\x69\x65\x6e\x74\x54\ +\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ +\x61\x74\x65\x28\x36\x2e\x31\x30\x32\x35\x20\x2d\x32\x36\x2e\x39\ +\x37\x38\x29\x22\x20\x67\x72\x61\x64\x69\x65\x6e\x74\x55\x6e\x69\ +\x74\x73\x3d\x22\x75\x73\x65\x72\x53\x70\x61\x63\x65\x4f\x6e\x55\ +\x73\x65\x22\x3e\x0a\x20\x20\x20\x3c\x73\x74\x6f\x70\x20\x73\x74\ +\x6f\x70\x2d\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x65\x61\x37\x62\x39\ +\x30\x22\x20\x6f\x66\x66\x73\x65\x74\x3d\x22\x30\x22\x2f\x3e\x0a\ +\x20\x20\x20\x3c\x73\x74\x6f\x70\x20\x73\x74\x6f\x70\x2d\x63\x6f\ +\x6c\x6f\x72\x3d\x22\x23\x62\x65\x31\x65\x33\x63\x22\x20\x6f\x66\ +\x66\x73\x65\x74\x3d\x22\x31\x22\x2f\x3e\x0a\x20\x20\x3c\x2f\x6c\ +\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x3e\x0a\x20\ +\x20\x3c\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\ +\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\ +\x65\x6e\x74\x39\x36\x37\x2d\x32\x2d\x36\x2d\x34\x2d\x36\x22\x20\ +\x78\x31\x3d\x22\x31\x33\x38\x2e\x33\x34\x22\x20\x78\x32\x3d\x22\ +\x36\x36\x2e\x33\x33\x35\x22\x20\x79\x31\x3d\x22\x31\x32\x32\x2e\ +\x34\x36\x22\x20\x79\x32\x3d\x22\x31\x39\x34\x2e\x34\x37\x22\x20\ +\x67\x72\x61\x64\x69\x65\x6e\x74\x54\x72\x61\x6e\x73\x66\x6f\x72\ +\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x36\x2e\x31\ +\x30\x32\x35\x20\x2d\x32\x36\x2e\x39\x37\x38\x29\x22\x20\x67\x72\ +\x61\x64\x69\x65\x6e\x74\x55\x6e\x69\x74\x73\x3d\x22\x75\x73\x65\ +\x72\x53\x70\x61\x63\x65\x4f\x6e\x55\x73\x65\x22\x3e\x0a\x20\x20\ +\x20\x3c\x73\x74\x6f\x70\x20\x73\x74\x6f\x70\x2d\x63\x6f\x6c\x6f\ +\x72\x3d\x22\x23\x36\x63\x36\x63\x36\x63\x22\x20\x6f\x66\x66\x73\ +\x65\x74\x3d\x22\x30\x22\x2f\x3e\x0a\x20\x20\x20\x3c\x73\x74\x6f\ +\x70\x20\x6f\x66\x66\x73\x65\x74\x3d\x22\x31\x22\x2f\x3e\x0a\x20\ +\x20\x3c\x2f\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\ +\x74\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\ +\x69\x65\x6e\x74\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x61\x72\x47\ +\x72\x61\x64\x69\x65\x6e\x74\x31\x30\x33\x33\x2d\x37\x2d\x31\x2d\ +\x30\x22\x20\x78\x31\x3d\x22\x31\x30\x37\x2e\x39\x31\x22\x20\x78\ +\x32\x3d\x22\x31\x32\x2e\x32\x33\x22\x20\x79\x31\x3d\x22\x31\x36\ +\x36\x2e\x38\x38\x22\x20\x79\x32\x3d\x22\x31\x38\x36\x2e\x36\x35\ +\x22\x20\x67\x72\x61\x64\x69\x65\x6e\x74\x54\x72\x61\x6e\x73\x66\ +\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x36\ +\x2e\x31\x30\x32\x35\x20\x2d\x32\x36\x2e\x39\x37\x38\x29\x22\x20\ +\x67\x72\x61\x64\x69\x65\x6e\x74\x55\x6e\x69\x74\x73\x3d\x22\x75\ +\x73\x65\x72\x53\x70\x61\x63\x65\x4f\x6e\x55\x73\x65\x22\x3e\x0a\ +\x20\x20\x20\x3c\x73\x74\x6f\x70\x20\x73\x74\x6f\x70\x2d\x63\x6f\ +\x6c\x6f\x72\x3d\x22\x23\x30\x30\x34\x32\x37\x64\x22\x20\x6f\x66\ +\x66\x73\x65\x74\x3d\x22\x30\x22\x2f\x3e\x0a\x20\x20\x20\x3c\x73\ +\x74\x6f\x70\x20\x73\x74\x6f\x70\x2d\x63\x6f\x6c\x6f\x72\x3d\x22\ +\x23\x30\x30\x37\x64\x65\x63\x22\x20\x6f\x66\x66\x73\x65\x74\x3d\ +\x22\x31\x22\x2f\x3e\x0a\x20\x20\x3c\x2f\x6c\x69\x6e\x65\x61\x72\ +\x47\x72\x61\x64\x69\x65\x6e\x74\x3e\x0a\x20\x20\x3c\x6c\x69\x6e\ +\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x20\x69\x64\x3d\x22\ +\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x31\x36\ +\x37\x31\x2d\x37\x2d\x35\x22\x20\x78\x31\x3d\x22\x33\x34\x2e\x36\ +\x38\x35\x22\x20\x78\x32\x3d\x22\x38\x32\x2e\x38\x35\x34\x22\x20\ +\x79\x31\x3d\x22\x39\x38\x2e\x36\x39\x35\x22\x20\x79\x32\x3d\x22\ +\x31\x36\x33\x2e\x33\x22\x20\x67\x72\x61\x64\x69\x65\x6e\x74\x54\ +\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ +\x61\x74\x65\x28\x36\x2e\x31\x30\x32\x35\x20\x2d\x32\x36\x2e\x39\ +\x37\x38\x29\x22\x20\x67\x72\x61\x64\x69\x65\x6e\x74\x55\x6e\x69\ +\x74\x73\x3d\x22\x75\x73\x65\x72\x53\x70\x61\x63\x65\x4f\x6e\x55\ +\x73\x65\x22\x3e\x0a\x20\x20\x20\x3c\x73\x74\x6f\x70\x20\x73\x74\ +\x6f\x70\x2d\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x65\x32\x30\x65\x31\ +\x66\x22\x20\x6f\x66\x66\x73\x65\x74\x3d\x22\x30\x22\x2f\x3e\x0a\ +\x20\x20\x20\x3c\x73\x74\x6f\x70\x20\x73\x74\x6f\x70\x2d\x63\x6f\ +\x6c\x6f\x72\x3d\x22\x23\x66\x32\x34\x36\x35\x37\x22\x20\x6f\x66\ +\x66\x73\x65\x74\x3d\x22\x31\x22\x2f\x3e\x0a\x20\x20\x3c\x2f\x6c\ +\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x3e\x0a\x20\ +\x20\x3c\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\ +\x20\x69\x64\x3d\x22\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\ +\x65\x6e\x74\x32\x39\x39\x38\x2d\x33\x22\x20\x78\x31\x3d\x22\x36\ +\x30\x2e\x31\x22\x20\x78\x32\x3d\x22\x31\x32\x31\x2e\x32\x39\x22\ +\x20\x79\x31\x3d\x22\x39\x32\x2e\x31\x35\x31\x22\x20\x79\x32\x3d\ +\x22\x34\x36\x2e\x30\x39\x31\x22\x20\x67\x72\x61\x64\x69\x65\x6e\ +\x74\x54\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ +\x73\x6c\x61\x74\x65\x28\x36\x2e\x31\x30\x32\x35\x20\x2d\x32\x36\ +\x2e\x39\x37\x38\x29\x22\x20\x67\x72\x61\x64\x69\x65\x6e\x74\x55\ +\x6e\x69\x74\x73\x3d\x22\x75\x73\x65\x72\x53\x70\x61\x63\x65\x4f\ +\x6e\x55\x73\x65\x22\x3e\x0a\x20\x20\x20\x3c\x73\x74\x6f\x70\x20\ +\x73\x74\x6f\x70\x2d\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x37\x36\x64\ +\x62\x65\x36\x22\x20\x6f\x66\x66\x73\x65\x74\x3d\x22\x30\x22\x2f\ +\x3e\x0a\x20\x20\x20\x3c\x73\x74\x6f\x70\x20\x73\x74\x6f\x70\x2d\ +\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x32\x30\x61\x31\x62\x31\x22\x20\ +\x6f\x66\x66\x73\x65\x74\x3d\x22\x31\x22\x2f\x3e\x0a\x20\x20\x3c\ +\x2f\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x3e\ +\x0a\x20\x20\x3c\x66\x69\x6c\x74\x65\x72\x20\x69\x64\x3d\x22\x66\ +\x69\x6c\x74\x65\x72\x33\x37\x32\x33\x2d\x39\x2d\x37\x2d\x38\x2d\ +\x33\x2d\x33\x2d\x30\x22\x20\x78\x3d\x22\x2d\x34\x2e\x35\x32\x34\ +\x31\x65\x2d\x35\x22\x20\x79\x3d\x22\x2d\x34\x2e\x31\x33\x33\x35\ +\x65\x2d\x35\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x2e\x30\x30\ +\x30\x31\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x2e\x30\x30\ +\x30\x31\x22\x20\x63\x6f\x6c\x6f\x72\x2d\x69\x6e\x74\x65\x72\x70\ +\x6f\x6c\x61\x74\x69\x6f\x6e\x2d\x66\x69\x6c\x74\x65\x72\x73\x3d\ +\x22\x73\x52\x47\x42\x22\x3e\x0a\x20\x20\x20\x3c\x66\x65\x47\x61\ +\x75\x73\x73\x69\x61\x6e\x42\x6c\x75\x72\x20\x73\x74\x64\x44\x65\ +\x76\x69\x61\x74\x69\x6f\x6e\x3d\x22\x30\x2e\x30\x30\x31\x33\x38\ +\x37\x37\x34\x30\x35\x22\x2f\x3e\x0a\x20\x20\x3c\x2f\x66\x69\x6c\ +\x74\x65\x72\x3e\x0a\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\x20\x3c\ +\x6d\x65\x74\x61\x64\x61\x74\x61\x3e\x0a\x20\x20\x3c\x72\x64\x66\ +\x3a\x52\x44\x46\x3e\x0a\x20\x20\x20\x3c\x63\x63\x3a\x57\x6f\x72\ +\x6b\x20\x72\x64\x66\x3a\x61\x62\x6f\x75\x74\x3d\x22\x22\x3e\x0a\ +\x20\x20\x20\x20\x3c\x64\x63\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x69\ +\x6d\x61\x67\x65\x2f\x73\x76\x67\x2b\x78\x6d\x6c\x3c\x2f\x64\x63\ +\x3a\x66\x6f\x72\x6d\x61\x74\x3e\x0a\x20\x20\x20\x20\x3c\x64\x63\ +\x3a\x74\x79\x70\x65\x20\x72\x64\x66\x3a\x72\x65\x73\x6f\x75\x72\ +\x63\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x70\x75\x72\x6c\x2e\ +\x6f\x72\x67\x2f\x64\x63\x2f\x64\x63\x6d\x69\x74\x79\x70\x65\x2f\ +\x53\x74\x69\x6c\x6c\x49\x6d\x61\x67\x65\x22\x2f\x3e\x0a\x20\x20\ +\x20\x20\x3c\x64\x63\x3a\x74\x69\x74\x6c\x65\x2f\x3e\x0a\x20\x20\ +\x20\x3c\x2f\x63\x63\x3a\x57\x6f\x72\x6b\x3e\x0a\x20\x20\x3c\x2f\ +\x72\x64\x66\x3a\x52\x44\x46\x3e\x0a\x20\x3c\x2f\x6d\x65\x74\x61\ +\x64\x61\x74\x61\x3e\x0a\x20\x3c\x67\x20\x74\x72\x61\x6e\x73\x66\ +\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\x78\x28\x2e\x32\x36\x34\ +\x35\x38\x20\x30\x20\x30\x20\x2e\x32\x36\x34\x35\x38\x20\x2d\x39\ +\x2e\x38\x38\x37\x34\x20\x2d\x32\x30\x2e\x32\x35\x38\x29\x22\x20\ +\x63\x6c\x69\x70\x2d\x70\x61\x74\x68\x3d\x22\x75\x72\x6c\x28\x23\ +\x63\x6c\x69\x70\x30\x2d\x33\x29\x22\x3e\x0a\x20\x20\x3c\x67\x20\ +\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ +\x6c\x61\x74\x65\x28\x35\x38\x30\x20\x33\x33\x30\x29\x22\x3e\x0a\ +\x20\x20\x20\x3c\x67\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\ +\x22\x6d\x61\x74\x72\x69\x78\x28\x2e\x30\x30\x32\x32\x34\x34\x20\ +\x2d\x2e\x33\x37\x30\x35\x39\x20\x2e\x33\x36\x34\x34\x31\x20\x2e\ +\x30\x30\x32\x32\x38\x32\x20\x2d\x35\x37\x32\x2e\x32\x38\x20\x31\ +\x37\x31\x2e\x33\x38\x29\x22\x3e\x0a\x20\x20\x20\x20\x3c\x67\x20\ +\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\x69\ +\x78\x28\x31\x2e\x31\x33\x39\x20\x30\x20\x30\x20\x31\x2e\x31\x33\ +\x39\x20\x35\x35\x37\x2e\x32\x35\x20\x31\x32\x35\x2e\x39\x29\x22\ +\x3e\x0a\x20\x20\x20\x20\x20\x3c\x67\x20\x74\x72\x61\x6e\x73\x66\ +\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x2d\ +\x2e\x37\x38\x32\x36\x38\x20\x2d\x35\x34\x36\x2e\x33\x33\x29\x22\ +\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x67\x20\x74\x72\x61\x6e\x73\ +\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\ +\x2d\x34\x31\x38\x2e\x30\x31\x20\x2d\x33\x32\x2e\x37\x33\x31\x29\ +\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x20\ +\x64\x3d\x22\x6d\x35\x35\x32\x2e\x35\x32\x20\x36\x35\x32\x2e\x33\ +\x31\x73\x35\x36\x2e\x36\x32\x32\x20\x31\x38\x2e\x30\x32\x32\x20\ +\x35\x35\x2e\x37\x32\x36\x20\x35\x39\x2e\x37\x33\x35\x63\x2d\x30\ +\x2e\x38\x36\x39\x32\x34\x20\x34\x30\x2e\x34\x34\x36\x2d\x35\x30\ +\x2e\x37\x38\x31\x20\x32\x36\x2e\x30\x35\x39\x2d\x35\x30\x2e\x37\ +\x38\x31\x20\x32\x36\x2e\x30\x35\x39\x73\x32\x37\x2e\x32\x38\x34\ +\x20\x31\x2e\x37\x34\x20\x32\x38\x2e\x31\x37\x34\x2d\x32\x35\x2e\ +\x35\x30\x31\x63\x30\x2e\x38\x33\x30\x36\x32\x2d\x32\x35\x2e\x34\ +\x32\x32\x2d\x33\x33\x2e\x31\x31\x38\x2d\x36\x30\x2e\x32\x39\x32\ +\x2d\x33\x33\x2e\x31\x31\x38\x2d\x36\x30\x2e\x32\x39\x32\x7a\x22\ +\x20\x66\x69\x6c\x6c\x3d\x22\x75\x72\x6c\x28\x23\x6c\x69\x6e\x65\ +\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x33\x32\x38\x34\x29\x22\ +\x20\x73\x74\x72\x6f\x6b\x65\x3d\x22\x23\x33\x64\x36\x65\x61\x32\ +\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\ +\x3d\x22\x2e\x39\x38\x30\x33\x39\x22\x20\x73\x74\x72\x6f\x6b\x65\ +\x2d\x77\x69\x64\x74\x68\x3d\x22\x2e\x32\x36\x34\x35\x38\x70\x78\ +\x22\x2f\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x2f\x67\x3e\x0a\x20\ +\x20\x20\x20\x20\x3c\x2f\x67\x3e\x0a\x20\x20\x20\x20\x20\x3c\x67\ +\x20\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\x68\x3d\x22\x2e\ +\x32\x36\x34\x35\x38\x70\x78\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\ +\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x6d\x38\x30\x2e\x39\x34\x32\ +\x20\x36\x39\x2e\x35\x39\x35\x73\x32\x30\x2e\x36\x36\x2d\x33\x30\ +\x2e\x39\x32\x20\x35\x37\x2e\x32\x36\x33\x2d\x32\x32\x2e\x34\x39\ +\x63\x33\x39\x2e\x36\x38\x36\x20\x39\x2e\x31\x33\x39\x39\x20\x34\ +\x30\x2e\x34\x34\x33\x20\x34\x37\x2e\x36\x32\x35\x20\x34\x30\x2e\ +\x34\x34\x33\x20\x34\x37\x2e\x36\x32\x35\x73\x2d\x33\x30\x2e\x30\ +\x39\x33\x2d\x32\x34\x2e\x36\x31\x32\x2d\x34\x38\x2e\x33\x38\x31\ +\x2d\x32\x38\x2e\x39\x31\x35\x63\x2d\x31\x36\x2e\x30\x35\x32\x2d\ +\x33\x2e\x37\x37\x37\x32\x2d\x34\x39\x2e\x33\x32\x36\x20\x33\x2e\ +\x37\x37\x39\x38\x2d\x34\x39\x2e\x33\x32\x36\x20\x33\x2e\x37\x37\ +\x39\x38\x7a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x75\x72\x6c\x28\x23\ +\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x31\x31\ +\x38\x39\x2d\x30\x2d\x36\x2d\x32\x29\x22\x20\x73\x74\x72\x6f\x6b\ +\x65\x3d\x22\x23\x63\x37\x33\x31\x34\x66\x22\x20\x73\x74\x72\x6f\ +\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x2e\x39\x35\x32\ +\x39\x34\x22\x2f\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x70\x61\x74\ +\x68\x20\x64\x3d\x22\x6d\x31\x34\x34\x2e\x34\x34\x20\x39\x35\x2e\ +\x34\x38\x37\x73\x31\x36\x2e\x33\x36\x32\x20\x33\x31\x2e\x32\x33\ +\x33\x2d\x36\x2e\x32\x33\x36\x36\x20\x35\x37\x2e\x34\x35\x32\x63\ +\x2d\x32\x38\x2e\x31\x37\x32\x20\x33\x32\x2e\x36\x38\x35\x2d\x36\ +\x35\x2e\x37\x36\x38\x20\x31\x34\x2e\x35\x35\x32\x2d\x36\x35\x2e\ +\x37\x36\x38\x20\x31\x34\x2e\x35\x35\x32\x73\x33\x30\x2e\x34\x32\ +\x36\x2d\x36\x2e\x39\x35\x38\x34\x20\x35\x30\x2e\x32\x37\x31\x2d\ +\x32\x38\x2e\x35\x33\x37\x63\x31\x34\x2e\x34\x39\x2d\x31\x35\x2e\ +\x37\x35\x36\x20\x32\x31\x2e\x37\x33\x34\x2d\x34\x33\x2e\x34\x36\ +\x37\x20\x32\x31\x2e\x37\x33\x34\x2d\x34\x33\x2e\x34\x36\x37\x7a\ +\x22\x20\x66\x69\x6c\x6c\x3d\x22\x75\x72\x6c\x28\x23\x6c\x69\x6e\ +\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\x74\x39\x36\x37\x2d\x32\ +\x2d\x36\x2d\x34\x2d\x36\x29\x22\x20\x73\x74\x72\x6f\x6b\x65\x3d\ +\x22\x23\x31\x36\x31\x36\x31\x36\x22\x2f\x3e\x0a\x20\x20\x20\x20\ +\x20\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x6d\x31\x31\x34\x2e\ +\x30\x31\x20\x31\x33\x39\x2e\x39\x73\x2d\x34\x37\x2e\x36\x37\x38\ +\x20\x34\x31\x2e\x37\x39\x31\x2d\x38\x35\x2e\x39\x39\x20\x31\x32\ +\x2e\x36\x36\x32\x63\x2d\x32\x30\x2e\x39\x39\x33\x2d\x31\x35\x2e\ +\x39\x36\x31\x20\x39\x2e\x34\x34\x39\x34\x2d\x35\x31\x2e\x30\x32\ +\x37\x20\x39\x2e\x34\x34\x39\x34\x2d\x35\x31\x2e\x30\x32\x37\x73\ +\x2d\x31\x35\x2e\x39\x37\x38\x20\x32\x33\x2e\x38\x39\x33\x20\x31\ +\x30\x2e\x35\x38\x33\x20\x33\x37\x2e\x36\x30\x39\x63\x31\x39\x2e\ +\x32\x35\x36\x20\x39\x2e\x39\x34\x33\x32\x20\x36\x35\x2e\x39\x35\ +\x37\x20\x30\x2e\x37\x35\x35\x39\x36\x20\x36\x35\x2e\x39\x35\x37\ +\x20\x30\x2e\x37\x35\x35\x39\x36\x7a\x22\x20\x66\x69\x6c\x6c\x3d\ +\x22\x75\x72\x6c\x28\x23\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\ +\x69\x65\x6e\x74\x31\x30\x33\x33\x2d\x37\x2d\x31\x2d\x30\x29\x22\ +\x20\x73\x74\x72\x6f\x6b\x65\x3d\x22\x23\x34\x34\x34\x22\x2f\x3e\ +\x0a\x20\x20\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\ +\x6d\x36\x33\x2e\x30\x33\x31\x20\x33\x39\x2e\x38\x34\x73\x2d\x38\ +\x2e\x32\x36\x30\x38\x20\x32\x30\x2e\x33\x31\x33\x2d\x31\x2e\x33\ +\x33\x36\x34\x20\x35\x32\x2e\x33\x38\x35\x63\x35\x2e\x32\x35\x37\ +\x36\x20\x32\x34\x2e\x33\x35\x32\x20\x32\x37\x2e\x32\x36\x31\x20\ +\x34\x34\x2e\x30\x39\x39\x20\x32\x37\x2e\x32\x36\x31\x20\x34\x34\ +\x2e\x30\x39\x39\x73\x2d\x33\x39\x2e\x35\x31\x39\x2d\x30\x2e\x38\ +\x31\x37\x35\x36\x2d\x34\x38\x2e\x33\x37\x36\x2d\x34\x31\x2e\x36\ +\x39\x34\x63\x2d\x37\x2e\x33\x37\x36\x34\x2d\x33\x34\x2e\x30\x34\ +\x34\x20\x32\x32\x2e\x34\x35\x31\x2d\x35\x34\x2e\x37\x39\x20\x32\ +\x32\x2e\x34\x35\x31\x2d\x35\x34\x2e\x37\x39\x7a\x22\x20\x66\x69\ +\x6c\x6c\x3d\x22\x75\x72\x6c\x28\x23\x6c\x69\x6e\x65\x61\x72\x47\ +\x72\x61\x64\x69\x65\x6e\x74\x31\x36\x37\x31\x2d\x37\x2d\x35\x29\ +\x22\x20\x73\x74\x72\x6f\x6b\x65\x3d\x22\x23\x65\x33\x30\x65\x31\ +\x66\x22\x20\x73\x74\x72\x6f\x6b\x65\x2d\x6f\x70\x61\x63\x69\x74\ +\x79\x3d\x22\x2e\x39\x38\x34\x33\x31\x22\x2f\x3e\x0a\x20\x20\x20\ +\x20\x20\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x6d\x31\x33\x38\ +\x2e\x33\x39\x20\x34\x31\x2e\x38\x31\x34\x73\x2d\x31\x34\x2e\x34\ +\x35\x38\x2d\x35\x33\x2e\x39\x37\x33\x2d\x35\x31\x2e\x30\x32\x37\ +\x2d\x32\x36\x2e\x30\x38\x63\x2d\x33\x30\x2e\x37\x39\x36\x20\x32\ +\x33\x2e\x34\x39\x2d\x32\x31\x2e\x31\x36\x37\x20\x37\x32\x2e\x35\ +\x37\x31\x2d\x32\x31\x2e\x31\x36\x37\x20\x37\x32\x2e\x35\x37\x31\ +\x73\x31\x35\x2e\x35\x35\x38\x2d\x34\x33\x2e\x33\x35\x34\x20\x33\ +\x34\x2e\x30\x31\x38\x2d\x35\x34\x2e\x38\x30\x37\x63\x32\x37\x2e\ +\x34\x36\x36\x2d\x31\x37\x2e\x30\x34\x20\x33\x38\x2e\x31\x37\x36\ +\x20\x38\x2e\x33\x31\x35\x35\x20\x33\x38\x2e\x31\x37\x36\x20\x38\ +\x2e\x33\x31\x35\x35\x7a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x75\x72\ +\x6c\x28\x23\x6c\x69\x6e\x65\x61\x72\x47\x72\x61\x64\x69\x65\x6e\ +\x74\x32\x39\x39\x38\x2d\x33\x29\x22\x20\x66\x69\x6c\x74\x65\x72\ +\x3d\x22\x75\x72\x6c\x28\x23\x66\x69\x6c\x74\x65\x72\x33\x37\x32\ +\x33\x2d\x39\x2d\x37\x2d\x38\x2d\x33\x2d\x33\x2d\x30\x29\x22\x20\ +\x73\x74\x72\x6f\x6b\x65\x3d\x22\x23\x33\x62\x62\x33\x63\x32\x22\ +\x2f\x3e\x0a\x20\x20\x20\x20\x20\x3c\x2f\x67\x3e\x0a\x20\x20\x20\ +\x20\x3c\x2f\x67\x3e\x0a\x20\x20\x20\x20\x3c\x67\x20\x63\x6c\x69\ +\x70\x2d\x70\x61\x74\x68\x3d\x22\x75\x72\x6c\x28\x23\x63\x6c\x69\ +\x70\x31\x35\x2d\x30\x39\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x3c\ +\x67\x20\x63\x6c\x69\x70\x2d\x70\x61\x74\x68\x3d\x22\x75\x72\x6c\ +\x28\x23\x63\x6c\x69\x70\x31\x36\x2d\x31\x29\x22\x3e\x0a\x20\x20\ +\x20\x20\x20\x20\x3c\x67\x20\x63\x6c\x69\x70\x2d\x70\x61\x74\x68\ +\x3d\x22\x75\x72\x6c\x28\x23\x63\x6c\x69\x70\x31\x37\x2d\x38\x39\ +\x29\x22\x3e\x0a\x20\x20\x20\x20\x20\x20\x20\x3c\x70\x61\x74\x68\ +\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x6d\x61\x74\x72\ +\x69\x78\x28\x31\x2e\x30\x30\x32\x36\x20\x30\x20\x30\x20\x31\x20\ +\x39\x37\x33\x20\x31\x36\x34\x29\x22\x20\x64\x3d\x22\x6d\x33\x35\ +\x2e\x30\x31\x33\x20\x32\x30\x2e\x34\x32\x34\x63\x2d\x39\x2e\x34\ +\x36\x30\x39\x20\x35\x2e\x39\x38\x30\x33\x2d\x32\x30\x2e\x34\x34\ +\x36\x20\x31\x37\x2e\x30\x34\x38\x2d\x32\x35\x2e\x39\x36\x36\x20\ +\x33\x33\x2e\x34\x31\x37\x2d\x34\x2e\x31\x35\x39\x37\x20\x31\x32\ +\x2e\x33\x33\x35\x2d\x34\x2e\x38\x31\x33\x32\x20\x32\x37\x2e\x30\ +\x36\x35\x2d\x30\x2e\x31\x30\x39\x36\x20\x34\x31\x2e\x31\x35\x35\ +\x20\x34\x2e\x31\x36\x39\x39\x20\x31\x32\x2e\x34\x39\x31\x20\x31\ +\x32\x2e\x34\x33\x36\x20\x32\x33\x2e\x39\x30\x39\x20\x32\x33\x2e\ +\x35\x38\x36\x20\x33\x31\x2e\x36\x37\x35\x20\x31\x30\x2e\x36\x31\ +\x32\x20\x37\x2e\x33\x39\x31\x20\x32\x33\x2e\x34\x38\x35\x20\x31\ +\x31\x2e\x32\x32\x32\x20\x33\x36\x2e\x30\x34\x34\x20\x31\x30\x2e\ +\x38\x36\x36\x20\x31\x33\x2e\x35\x33\x38\x2d\x30\x2e\x33\x30\x33\ +\x20\x32\x36\x2e\x34\x30\x33\x2d\x35\x2e\x34\x33\x20\x33\x35\x2e\ +\x39\x33\x32\x2d\x31\x33\x2e\x35\x33\x32\x20\x31\x30\x2e\x30\x33\ +\x31\x2d\x38\x2e\x35\x32\x39\x20\x31\x36\x2e\x32\x36\x34\x2d\x32\ +\x30\x2e\x30\x39\x37\x20\x31\x38\x2e\x34\x32\x31\x2d\x33\x31\x2e\ +\x33\x38\x35\x20\x32\x2e\x34\x34\x2d\x31\x32\x2e\x37\x36\x34\x2d\ +\x30\x2e\x31\x33\x33\x2d\x32\x35\x2e\x30\x30\x33\x2d\x34\x2e\x38\ +\x38\x39\x2d\x33\x34\x2e\x32\x30\x35\x2d\x33\x2e\x31\x30\x33\x2d\ +\x36\x2e\x30\x30\x33\x32\x2d\x37\x2e\x31\x37\x35\x2d\x31\x30\x2e\ +\x39\x32\x35\x2d\x31\x31\x2e\x33\x34\x38\x2d\x31\x34\x2e\x37\x35\ +\x35\x2d\x34\x2e\x31\x38\x2d\x33\x2e\x38\x33\x36\x35\x2d\x38\x2e\ +\x34\x32\x35\x34\x2d\x36\x2e\x35\x35\x37\x32\x2d\x31\x32\x2e\x31\ +\x37\x34\x2d\x38\x2e\x35\x31\x32\x37\x2d\x30\x2e\x30\x31\x39\x39\ +\x2d\x30\x2e\x30\x31\x30\x34\x20\x39\x2e\x30\x35\x32\x39\x2d\x31\ +\x37\x2e\x36\x33\x34\x20\x39\x2e\x30\x32\x33\x39\x2d\x31\x37\x2e\ +\x36\x34\x39\x20\x30\x2e\x30\x32\x39\x20\x30\x2e\x30\x31\x34\x39\ +\x20\x39\x2e\x32\x37\x34\x2d\x31\x37\x2e\x35\x31\x39\x20\x39\x2e\ +\x33\x31\x31\x2d\x31\x37\x2e\x35\x20\x36\x2e\x39\x35\x37\x20\x33\ +\x2e\x36\x32\x38\x39\x20\x31\x34\x2e\x34\x20\x38\x2e\x38\x37\x30\ +\x32\x20\x32\x31\x2e\x33\x32\x37\x20\x31\x36\x2e\x31\x32\x20\x36\ +\x2e\x38\x39\x31\x20\x37\x2e\x32\x31\x31\x31\x20\x31\x33\x2e\x32\ +\x36\x39\x20\x31\x36\x2e\x34\x33\x33\x20\x31\x37\x2e\x36\x34\x34\ +\x20\x32\x37\x2e\x34\x39\x39\x20\x36\x2e\x36\x37\x37\x20\x31\x36\ +\x2e\x38\x39\x32\x20\x38\x2e\x32\x38\x34\x20\x33\x37\x2e\x35\x30\ +\x34\x20\x31\x2e\x38\x31\x20\x35\x37\x2e\x33\x39\x33\x2d\x35\x2e\ +\x37\x33\x38\x20\x31\x37\x2e\x36\x32\x38\x2d\x31\x37\x2e\x36\x35\ +\x39\x20\x33\x33\x2e\x36\x33\x32\x2d\x33\x33\x2e\x39\x33\x36\x20\ +\x34\x34\x2e\x30\x37\x33\x2d\x31\x35\x2e\x34\x39\x33\x20\x39\x2e\ +\x39\x33\x38\x2d\x33\x34\x2e\x32\x32\x34\x20\x31\x34\x2e\x32\x39\ +\x31\x2d\x35\x32\x2e\x32\x31\x37\x20\x31\x32\x2e\x32\x35\x34\x2d\ +\x31\x36\x2e\x36\x37\x32\x2d\x31\x2e\x39\x36\x34\x2d\x33\x32\x2e\ +\x30\x37\x32\x2d\x39\x2e\x33\x34\x38\x2d\x34\x33\x2e\x36\x31\x35\ +\x2d\x32\x30\x2e\x30\x38\x31\x2d\x31\x32\x2e\x31\x33\x37\x2d\x31\ +\x31\x2e\x32\x38\x35\x2d\x31\x39\x2e\x36\x36\x38\x2d\x32\x35\x2e\ +\x38\x35\x39\x2d\x32\x32\x2e\x35\x30\x31\x2d\x34\x30\x2e\x32\x39\ +\x32\x2d\x33\x2e\x31\x39\x34\x39\x2d\x31\x36\x2e\x32\x37\x38\x2d\ +\x30\x2e\x34\x35\x30\x39\x32\x2d\x33\x31\x2e\x38\x38\x20\x35\x2e\ +\x31\x30\x38\x2d\x34\x34\x2e\x31\x34\x35\x20\x37\x2e\x33\x35\x31\ +\x32\x2d\x31\x36\x2e\x32\x32\x20\x31\x39\x2e\x32\x32\x39\x2d\x32\ +\x36\x2e\x35\x30\x36\x20\x32\x38\x2e\x35\x34\x38\x2d\x33\x32\x2e\ +\x33\x39\x37\x7a\x22\x20\x66\x69\x6c\x6c\x3d\x22\x23\x62\x66\x62\ +\x66\x62\x66\x22\x2f\x3e\x0a\x20\x20\x20\x20\x20\x20\x3c\x2f\x67\ +\x3e\x0a\x20\x20\x20\x20\x20\x3c\x2f\x67\x3e\x0a\x20\x20\x20\x20\ +\x3c\x2f\x67\x3e\x0a\x20\x20\x20\x20\x3c\x67\x20\x66\x69\x6c\x6c\ +\x3d\x22\x23\x62\x66\x62\x66\x62\x66\x22\x3e\x0a\x20\x20\x20\x20\ +\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x6d\x32\x32\x31\x2e\x37\ +\x31\x20\x31\x35\x39\x2e\x35\x76\x31\x36\x30\x2e\x34\x36\x68\x2d\ +\x34\x33\x2e\x37\x30\x33\x76\x2d\x31\x36\x30\x2e\x34\x36\x7a\x6d\ +\x2d\x36\x36\x2e\x39\x39\x20\x31\x31\x39\x2e\x33\x31\x20\x31\x35\ +\x2e\x33\x31\x32\x20\x32\x37\x2e\x37\x35\x33\x76\x30\x2e\x33\x31\ +\x39\x71\x2d\x31\x36\x2e\x35\x38\x38\x20\x31\x34\x2e\x36\x37\x34\ +\x2d\x34\x31\x2e\x31\x35\x31\x20\x31\x34\x2e\x36\x37\x34\x2d\x33\ +\x38\x2e\x39\x31\x38\x20\x30\x2d\x35\x35\x2e\x31\x38\x37\x2d\x32\ +\x31\x2e\x30\x35\x34\x2d\x31\x34\x2e\x39\x39\x33\x2d\x31\x38\x2e\ +\x38\x32\x31\x2d\x31\x34\x2e\x39\x39\x33\x2d\x35\x39\x2e\x39\x37\ +\x32\x76\x2d\x38\x31\x2e\x30\x32\x36\x68\x34\x33\x2e\x37\x30\x33\ +\x76\x38\x31\x2e\x30\x32\x36\x71\x30\x20\x38\x2e\x36\x31\x33\x20\ +\x30\x2e\x33\x31\x39\x20\x31\x36\x2e\x35\x38\x38\x20\x30\x2e\x36\ +\x33\x38\x20\x37\x2e\x39\x37\x35\x20\x33\x2e\x35\x30\x39\x20\x31\ +\x34\x2e\x30\x33\x36\x20\x33\x2e\x31\x39\x20\x36\x2e\x30\x36\x31\ +\x20\x39\x2e\x35\x37\x20\x39\x2e\x38\x38\x39\x20\x36\x2e\x33\x38\ +\x20\x33\x2e\x35\x30\x39\x20\x31\x38\x2e\x31\x38\x33\x20\x33\x2e\ +\x35\x30\x39\x20\x35\x2e\x34\x32\x33\x20\x30\x20\x31\x30\x2e\x35\ +\x32\x37\x2d\x31\x2e\x35\x39\x35\x74\x39\x2e\x35\x37\x2d\x34\x2e\ +\x31\x34\x37\x7a\x22\x2f\x3e\x0a\x20\x20\x20\x20\x20\x3c\x70\x61\ +\x74\x68\x20\x64\x3d\x22\x6d\x31\x37\x38\x2e\x30\x39\x20\x31\x35\ +\x39\x2e\x35\x68\x34\x33\x2e\x33\x38\x34\x76\x31\x35\x39\x2e\x35\ +\x68\x2d\x34\x33\x2e\x33\x38\x34\x7a\x6d\x31\x34\x37\x2e\x33\x38\ +\x20\x31\x38\x2e\x35\x30\x32\x71\x37\x2e\x39\x37\x35\x20\x39\x2e\ +\x38\x38\x39\x20\x31\x31\x2e\x34\x38\x34\x20\x32\x34\x2e\x38\x38\ +\x32\x74\x33\x2e\x35\x30\x39\x20\x33\x37\x2e\x30\x30\x34\x76\x37\ +\x39\x2e\x31\x31\x32\x68\x2d\x34\x33\x2e\x33\x38\x34\x76\x2d\x37\ +\x39\x2e\x31\x31\x32\x71\x30\x2d\x38\x2e\x39\x33\x32\x2d\x30\x2e\ +\x36\x33\x38\x2d\x31\x36\x2e\x39\x30\x37\x2d\x30\x2e\x33\x31\x39\ +\x2d\x38\x2e\x32\x39\x34\x2d\x33\x2e\x31\x39\x2d\x31\x34\x2e\x33\ +\x35\x35\x2d\x32\x2e\x38\x37\x31\x2d\x36\x2e\x33\x38\x2d\x39\x2e\ +\x32\x35\x31\x2d\x39\x2e\x38\x38\x39\x74\x2d\x31\x38\x2e\x31\x38\ +\x33\x2d\x33\x2e\x35\x30\x39\x71\x2d\x31\x31\x2e\x31\x36\x35\x20\ +\x30\x2d\x32\x30\x2e\x37\x33\x35\x20\x34\x2e\x37\x38\x35\x76\x30\ +\x2e\x33\x31\x39\x6c\x2d\x30\x2e\x33\x31\x39\x2d\x30\x2e\x33\x31\ +\x39\x2d\x31\x35\x2e\x36\x33\x31\x2d\x32\x37\x2e\x37\x35\x33\x20\ +\x30\x2e\x33\x31\x39\x2d\x30\x2e\x33\x31\x39\x71\x31\x36\x2e\x35\ +\x38\x38\x2d\x31\x34\x2e\x39\x39\x33\x20\x34\x31\x2e\x34\x37\x2d\ +\x31\x34\x2e\x39\x39\x33\x20\x33\x38\x2e\x32\x38\x20\x30\x20\x35\ +\x34\x2e\x35\x34\x39\x20\x32\x31\x2e\x30\x35\x34\x7a\x22\x2f\x3e\ +\x0a\x20\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x6d\ +\x33\x35\x38\x2e\x30\x31\x20\x33\x31\x39\x76\x2d\x31\x35\x39\x2e\ +\x35\x68\x34\x32\x2e\x34\x32\x37\x76\x31\x35\x39\x2e\x35\x7a\x6d\ +\x30\x2d\x32\x32\x33\x2e\x33\x68\x34\x32\x2e\x34\x32\x37\x76\x33\ +\x37\x2e\x33\x32\x33\x68\x2d\x34\x32\x2e\x34\x32\x37\x7a\x22\x2f\ +\x3e\x0a\x20\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\ +\x6d\x35\x33\x36\x2e\x30\x31\x20\x32\x35\x38\x2e\x30\x37\x20\x33\ +\x34\x2e\x34\x35\x32\x20\x32\x32\x2e\x36\x34\x39\x2d\x30\x2e\x33\ +\x31\x39\x20\x30\x2e\x33\x31\x39\x71\x2d\x31\x31\x2e\x31\x36\x35\ +\x20\x31\x39\x2e\x31\x34\x2d\x33\x30\x2e\x33\x30\x35\x20\x32\x39\ +\x2e\x39\x38\x36\x2d\x31\x38\x2e\x38\x32\x31\x20\x31\x30\x2e\x38\ +\x34\x36\x2d\x34\x30\x2e\x38\x33\x32\x20\x31\x30\x2e\x38\x34\x36\ +\x2d\x31\x37\x2e\x32\x32\x36\x20\x30\x2d\x33\x32\x2e\x32\x31\x39\ +\x2d\x36\x2e\x33\x38\x2d\x31\x34\x2e\x39\x39\x33\x2d\x36\x2e\x36\ +\x39\x39\x2d\x32\x36\x2e\x31\x35\x38\x2d\x31\x37\x2e\x38\x36\x34\ +\x74\x2d\x31\x37\x2e\x38\x36\x34\x2d\x32\x36\x2e\x31\x35\x38\x71\ +\x2d\x36\x2e\x33\x38\x2d\x31\x34\x2e\x39\x39\x33\x2d\x36\x2e\x33\ +\x38\x2d\x33\x31\x2e\x39\x74\x36\x2e\x33\x38\x2d\x33\x31\x2e\x39\ +\x71\x36\x2e\x36\x39\x39\x2d\x31\x34\x2e\x39\x39\x33\x20\x31\x37\ +\x2e\x38\x36\x34\x2d\x32\x36\x2e\x31\x35\x38\x74\x32\x36\x2e\x31\ +\x35\x38\x2d\x31\x37\x2e\x35\x34\x35\x71\x31\x34\x2e\x39\x39\x33\ +\x2d\x36\x2e\x36\x39\x39\x20\x33\x32\x2e\x32\x31\x39\x2d\x36\x2e\ +\x36\x39\x39\x20\x32\x32\x2e\x30\x31\x31\x20\x30\x20\x34\x30\x2e\ +\x38\x33\x32\x20\x31\x30\x2e\x38\x34\x36\x20\x31\x39\x2e\x31\x34\ +\x20\x31\x30\x2e\x38\x34\x36\x20\x33\x30\x2e\x33\x30\x35\x20\x32\ +\x39\x2e\x39\x38\x36\x6c\x30\x2e\x33\x31\x39\x20\x30\x2e\x33\x31\ +\x39\x2d\x33\x34\x2e\x37\x37\x31\x20\x32\x32\x2e\x36\x34\x39\x2d\ +\x30\x2e\x33\x31\x39\x2d\x30\x2e\x33\x31\x39\x71\x2d\x35\x2e\x31\ +\x30\x34\x2d\x31\x31\x2e\x38\x30\x33\x2d\x31\x34\x2e\x39\x39\x33\ +\x2d\x31\x39\x2e\x31\x34\x2d\x39\x2e\x35\x37\x2d\x37\x2e\x33\x33\ +\x37\x2d\x32\x31\x2e\x33\x37\x33\x2d\x37\x2e\x33\x33\x37\x2d\x38\ +\x2e\x36\x31\x33\x20\x30\x2d\x31\x36\x2e\x32\x36\x39\x20\x33\x2e\ +\x38\x32\x38\x2d\x37\x2e\x33\x33\x37\x20\x33\x2e\x35\x30\x39\x2d\ +\x31\x32\x2e\x37\x36\x20\x39\x2e\x38\x38\x39\x2d\x35\x2e\x34\x32\ +\x33\x20\x36\x2e\x30\x36\x31\x2d\x38\x2e\x36\x31\x33\x20\x31\x34\ +\x2e\x33\x35\x35\x2d\x33\x2e\x31\x39\x20\x37\x2e\x39\x37\x35\x2d\ +\x33\x2e\x31\x39\x20\x31\x37\x2e\x32\x32\x36\x74\x33\x2e\x31\x39\ +\x20\x31\x37\x2e\x32\x32\x36\x20\x38\x2e\x36\x31\x33\x20\x31\x34\ +\x2e\x30\x33\x36\x71\x35\x2e\x34\x32\x33\x20\x35\x2e\x37\x34\x32\ +\x20\x31\x32\x2e\x37\x36\x20\x39\x2e\x32\x35\x31\x20\x37\x2e\x36\ +\x35\x36\x20\x33\x2e\x31\x39\x20\x31\x36\x2e\x32\x36\x39\x20\x33\ +\x2e\x31\x39\x20\x31\x31\x2e\x38\x30\x33\x20\x30\x20\x32\x31\x2e\ +\x33\x37\x33\x2d\x36\x2e\x33\x38\x20\x39\x2e\x38\x38\x39\x2d\x36\ +\x2e\x36\x39\x39\x20\x31\x34\x2e\x39\x39\x33\x2d\x31\x38\x2e\x35\ +\x30\x32\x6c\x30\x2e\x33\x31\x39\x2d\x30\x2e\x36\x33\x38\x7a\x22\ +\x2f\x3e\x0a\x20\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x20\x64\x3d\ +\x22\x6d\x39\x35\x38\x2e\x34\x33\x20\x39\x35\x2e\x37\x76\x32\x32\ +\x33\x2e\x33\x68\x2d\x34\x32\x2e\x34\x32\x37\x6c\x30\x2e\x33\x31\ +\x39\x2d\x38\x30\x2e\x30\x36\x39\x71\x30\x2d\x38\x2e\x39\x33\x32\ +\x2d\x33\x2e\x38\x32\x38\x2d\x31\x36\x2e\x39\x30\x37\x2d\x33\x2e\ +\x35\x30\x39\x2d\x37\x2e\x39\x37\x35\x2d\x39\x2e\x35\x37\x2d\x31\ +\x33\x2e\x37\x31\x37\x2d\x35\x2e\x37\x34\x32\x2d\x36\x2e\x30\x36\ +\x31\x2d\x31\x34\x2e\x30\x33\x36\x2d\x39\x2e\x32\x35\x31\x2d\x37\ +\x2e\x39\x37\x35\x2d\x33\x2e\x35\x30\x39\x2d\x31\x37\x2e\x32\x32\ +\x36\x2d\x33\x2e\x35\x30\x39\x74\x2d\x31\x37\x2e\x32\x32\x36\x20\ +\x33\x2e\x35\x30\x39\x71\x2d\x37\x2e\x39\x37\x35\x20\x33\x2e\x31\ +\x39\x2d\x31\x34\x2e\x30\x33\x36\x20\x39\x2e\x32\x35\x31\x2d\x35\ +\x2e\x37\x34\x32\x20\x36\x2e\x30\x36\x31\x2d\x39\x2e\x32\x35\x31\ +\x20\x31\x34\x2e\x30\x33\x36\x74\x2d\x33\x2e\x35\x30\x39\x20\x31\ +\x37\x2e\x32\x32\x36\x20\x33\x2e\x35\x30\x39\x20\x31\x37\x2e\x32\ +\x32\x36\x20\x39\x2e\x32\x35\x31\x20\x31\x34\x2e\x30\x33\x36\x71\ +\x36\x2e\x30\x36\x31\x20\x35\x2e\x37\x34\x32\x20\x31\x34\x2e\x30\ +\x33\x36\x20\x39\x2e\x32\x35\x31\x74\x31\x37\x2e\x32\x32\x36\x20\ +\x33\x2e\x35\x30\x39\x71\x34\x2e\x34\x36\x36\x20\x30\x20\x38\x2e\ +\x39\x33\x32\x2d\x30\x2e\x36\x33\x38\x74\x38\x2e\x32\x39\x34\x2d\ +\x32\x2e\x35\x35\x32\x68\x30\x2e\x36\x33\x38\x6c\x31\x35\x2e\x36\ +\x33\x31\x20\x32\x38\x2e\x37\x31\x2d\x30\x2e\x33\x31\x39\x20\x30\ +\x2e\x33\x31\x39\x71\x2d\x31\x35\x2e\x36\x33\x31\x20\x31\x32\x2e\ +\x31\x32\x32\x2d\x33\x38\x2e\x32\x38\x20\x31\x32\x2e\x31\x32\x32\ +\x2d\x31\x37\x2e\x32\x32\x36\x20\x30\x2d\x33\x32\x2e\x32\x31\x39\ +\x2d\x36\x2e\x33\x38\x74\x2d\x32\x36\x2e\x31\x35\x38\x2d\x31\x37\ +\x2e\x35\x34\x35\x2d\x31\x37\x2e\x38\x36\x34\x2d\x32\x36\x2e\x31\ +\x35\x38\x71\x2d\x36\x2e\x33\x38\x2d\x31\x34\x2e\x39\x39\x33\x2d\ +\x36\x2e\x33\x38\x2d\x33\x31\x2e\x39\x74\x36\x2e\x33\x38\x2d\x33\ +\x31\x2e\x39\x71\x36\x2e\x36\x39\x39\x2d\x31\x34\x2e\x39\x39\x33\ +\x20\x31\x37\x2e\x38\x36\x34\x2d\x32\x36\x2e\x31\x35\x38\x74\x32\ +\x36\x2e\x31\x35\x38\x2d\x31\x37\x2e\x35\x34\x35\x20\x33\x32\x2e\ +\x32\x31\x39\x2d\x36\x2e\x33\x38\x71\x33\x32\x2e\x35\x33\x38\x20\ +\x30\x20\x34\x39\x2e\x37\x36\x34\x20\x32\x34\x2e\x32\x34\x34\x76\ +\x2d\x38\x36\x2e\x31\x33\x7a\x22\x2f\x3e\x0a\x20\x20\x20\x20\x20\ +\x3c\x70\x61\x74\x68\x20\x64\x3d\x22\x6d\x39\x38\x35\x2e\x30\x39\ +\x20\x31\x33\x33\x2e\x34\x20\x30\x2e\x30\x31\x30\x31\x20\x34\x65\ +\x2d\x33\x63\x2d\x33\x2e\x31\x34\x35\x35\x20\x37\x2e\x32\x31\x35\ +\x33\x20\x31\x38\x2e\x34\x34\x31\x20\x31\x36\x2e\x33\x31\x20\x32\ +\x38\x2e\x39\x30\x38\x20\x32\x32\x2e\x30\x31\x35\x6c\x30\x2e\x34\ +\x38\x36\x38\x20\x39\x2e\x30\x31\x31\x33\x63\x2d\x31\x2e\x34\x31\ +\x39\x36\x2d\x31\x2e\x34\x30\x39\x38\x2d\x32\x2e\x37\x37\x37\x35\ +\x2d\x31\x2e\x39\x30\x33\x32\x2d\x34\x2e\x31\x33\x33\x31\x2d\x32\ +\x2e\x35\x39\x30\x39\x2d\x32\x2e\x34\x30\x31\x39\x2d\x31\x2e\x30\ +\x30\x36\x32\x2d\x35\x2e\x37\x37\x36\x31\x20\x34\x2e\x34\x35\x33\ +\x31\x2d\x32\x2e\x36\x35\x34\x33\x20\x36\x2e\x31\x33\x33\x35\x20\ +\x32\x2e\x34\x34\x35\x31\x20\x31\x2e\x35\x33\x36\x34\x20\x34\x2e\ +\x36\x39\x33\x35\x20\x32\x2e\x30\x33\x38\x20\x36\x2e\x37\x38\x39\ +\x34\x20\x32\x2e\x34\x35\x38\x6c\x2d\x30\x2e\x36\x34\x36\x39\x20\ +\x33\x33\x2e\x33\x36\x31\x20\x33\x2e\x37\x36\x36\x35\x20\x31\x2e\ +\x38\x33\x37\x31\x20\x31\x35\x2e\x36\x36\x2d\x33\x39\x2e\x37\x35\ +\x20\x31\x37\x2e\x39\x31\x38\x20\x39\x2e\x32\x33\x35\x35\x20\x34\ +\x2e\x36\x39\x37\x32\x20\x31\x37\x2e\x31\x31\x39\x20\x31\x2e\x39\ +\x35\x37\x39\x20\x31\x2e\x35\x31\x39\x33\x20\x35\x2e\x33\x36\x32\ +\x35\x2d\x31\x39\x2e\x34\x32\x31\x20\x31\x32\x2e\x39\x32\x34\x2d\ +\x31\x35\x2e\x34\x35\x35\x2d\x32\x2e\x33\x36\x32\x32\x2d\x30\x2e\ +\x37\x34\x37\x38\x38\x2d\x31\x36\x2e\x37\x35\x33\x20\x35\x2e\x38\ +\x37\x34\x36\x2d\x31\x37\x2e\x37\x38\x37\x2d\x39\x2e\x34\x38\x35\ +\x31\x20\x32\x33\x2e\x37\x38\x38\x2d\x33\x35\x2e\x34\x38\x38\x2d\ +\x33\x2e\x36\x35\x32\x32\x2d\x32\x2e\x30\x35\x34\x39\x2d\x32\x37\ +\x2e\x30\x37\x32\x20\x31\x39\x2e\x35\x30\x36\x63\x2d\x31\x2e\x35\ +\x33\x37\x33\x2d\x31\x2e\x34\x38\x35\x2d\x33\x2e\x32\x32\x38\x37\ +\x2d\x33\x2e\x30\x34\x39\x31\x2d\x35\x2e\x38\x38\x32\x39\x2d\x34\ +\x2e\x31\x38\x36\x34\x2d\x33\x2e\x31\x35\x37\x36\x2d\x31\x2e\x36\ +\x31\x32\x33\x2d\x35\x2e\x37\x32\x39\x20\x34\x2e\x32\x36\x38\x32\ +\x2d\x33\x2e\x35\x33\x35\x33\x20\x35\x2e\x36\x37\x31\x35\x20\x31\ +\x2e\x33\x33\x35\x38\x20\x30\x2e\x37\x32\x35\x33\x37\x20\x32\x2e\ +\x35\x31\x33\x39\x20\x31\x2e\x35\x36\x31\x37\x20\x34\x2e\x34\x38\ +\x30\x38\x20\x31\x2e\x39\x32\x37\x37\x6c\x2d\x37\x2e\x36\x38\x38\ +\x20\x34\x2e\x37\x32\x32\x38\x63\x2d\x31\x30\x2e\x36\x34\x35\x2d\ +\x35\x2e\x33\x36\x34\x35\x2d\x33\x30\x2e\x34\x30\x34\x2d\x31\x37\ +\x2e\x39\x34\x36\x2d\x33\x34\x2e\x35\x35\x2d\x31\x31\x2e\x32\x35\ +\x35\x6c\x2d\x30\x2e\x30\x31\x30\x31\x2d\x34\x65\x2d\x33\x63\x34\ +\x2e\x33\x65\x2d\x34\x20\x37\x65\x2d\x33\x20\x39\x2e\x32\x65\x2d\ +\x34\x20\x30\x2e\x30\x31\x34\x34\x2d\x39\x65\x2d\x33\x20\x30\x2e\ +\x30\x32\x34\x36\x20\x33\x2e\x37\x65\x2d\x34\x20\x36\x65\x2d\x33\ +\x20\x2d\x30\x2e\x30\x31\x30\x31\x20\x30\x2e\x30\x31\x31\x32\x2d\ +\x30\x2e\x30\x31\x33\x36\x20\x30\x2e\x30\x31\x38\x7a\x22\x2f\x3e\ +\x0a\x20\x20\x20\x20\x3c\x2f\x67\x3e\x0a\x20\x20\x20\x3c\x2f\x67\ +\x3e\x0a\x20\x20\x3c\x2f\x67\x3e\x0a\x20\x3c\x2f\x67\x3e\x0a\x3c\ +\x2f\x73\x76\x67\x3e\x0a\ +" + +qt_resource_name = b"\ +\x00\x09\ +\x0c\x78\x54\x88\ +\x00\x6e\ +\x00\x65\x00\x77\x00\x50\x00\x72\x00\x65\x00\x66\x00\x69\x00\x78\ +\x00\x0f\ +\x0b\x2f\xcc\x47\ +\x00\x75\ +\x00\x6e\x00\x69\x00\x63\x00\x61\x00\x64\x00\x6f\x00\x49\x00\x43\x00\x4f\x00\x4e\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x11\ +\x08\x93\x84\x47\ +\x00\x55\ +\x00\x4e\x00\x49\x00\x43\x00\x41\x00\x44\x00\x4f\x00\x4c\x00\x6f\x00\x67\x00\x6f\x00\x39\x00\x30\x00\x2e\x00\x73\x00\x76\x00\x67\ +\ +" + +qt_resource_struct_v1 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\ +\x00\x00\x00\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x25\x6f\ +\x00\x00\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +qt_resource_struct_v2 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x3c\x00\x00\x00\x00\x00\x01\x00\x00\x25\x6f\ +\x00\x00\x01\x78\x87\xfe\x9e\x5b\ +\x00\x00\x00\x18\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x78\x64\x9b\x11\xd4\ +" + +qt_version = [int(v) for v in QtCore.qVersion().split('.')] +if qt_version < [5, 8, 0]: + rcc_version = 1 + qt_resource_struct = qt_resource_struct_v1 +else: + rcc_version = 2 + qt_resource_struct = qt_resource_struct_v2 + +def qInitResources(): + QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/installer/own_python_packages.txt b/installer/own_python_packages.txt new file mode 100644 index 0000000000000000000000000000000000000000..434116936b8a5ac58c8de3c1ac6ff3bf71a46ca9 --- /dev/null +++ b/installer/own_python_packages.txt @@ -0,0 +1,6 @@ +pyaircraftgeometry2 +pyaixml +pycoordinatesystemconversion +pyenergycarriers +pymodulepackage +pyunitconversion \ No newline at end of file diff --git a/installer/python_module_list.txt b/installer/python_module_list.txt new file mode 100644 index 0000000000000000000000000000000000000000..f2a0771c0adda20ab1322aeeb182cc9825bde23a --- /dev/null +++ b/installer/python_module_list.txt @@ -0,0 +1,5 @@ +fuselage_design +tank_design +landing_gear_design +weight_and_balance_analysis +cost_estimation \ No newline at end of file diff --git a/installer/standard_python_packages.txt b/installer/standard_python_packages.txt new file mode 100644 index 0000000000000000000000000000000000000000..2478e03a2c0f6a65d43c91cc404c83cde3be1fca --- /dev/null +++ b/installer/standard_python_packages.txt @@ -0,0 +1,10 @@ +numpy +pandas +scipy +statsmodels +ambiance +matplotlib +yattag +termcolor +bs4 +openpyxl \ No newline at end of file diff --git a/installer/sub_functions/abort_install.py b/installer/sub_functions/abort_install.py new file mode 100644 index 0000000000000000000000000000000000000000..72ddbc1852cca393bfd0c0983fa4062e33875b07 --- /dev/null +++ b/installer/sub_functions/abort_install.py @@ -0,0 +1,41 @@ +def abort_install(self): + """ Call function to handle the cancel button. + + :rtype: object + """ + + ''' imports for python ''' + import os + import shutil + import PyQt5.QtWidgets + + check_finished = self.cancel_button.text() + check_uninstall = self.uninstall_button.isVisible() + check_welcome_panel = self.welcome_panel.isVisible() + check_integration_panel = self.integration_panel.isVisible() + + if not check_finished == 'Finish' and not check_uninstall and not check_welcome_panel\ + and not check_integration_panel: + install_folder = self.install_path_line_edit.text() + install_folder = install_folder.replace(os.sep, '/') + + if os.path.isdir(install_folder): + shutil.rmtree(install_folder) + last_character = install_folder[-1] + + if not (last_character == '/'): + install_folder = install_folder + '/' + + indices = [] + i = install_folder.find('/') + while i >= 0: + indices.append(i) + i = install_folder.find('/', i + 1) + parent_folder = install_folder[:indices[-2]] + check_flag_parent_folder = os.path.isdir(parent_folder) + + if check_flag_parent_folder: + if not os.listdir(parent_folder): + shutil.rmtree(parent_folder) + + PyQt5.QtWidgets.QApplication.quit() diff --git a/installer/sub_functions/browse_folder.py b/installer/sub_functions/browse_folder.py new file mode 100644 index 0000000000000000000000000000000000000000..04e5eefc531b98cf3b26f4689b62eac07f7fc47b --- /dev/null +++ b/installer/sub_functions/browse_folder.py @@ -0,0 +1,171 @@ +def browse_folder(self): + """ Call function to handle the browse button. + + :rtype: object + """ + + ''' imports for python ''' + import os + import shutil + import configparser + from PyQt5.QtCore import QCoreApplication + from PyQt5.QtWidgets import QFileDialog + + # read user path string from browse edit-field of installer application + install_path_panel_visibility = self.install_path_panel.isVisible() + repository_path_panel_visibility = self.repository_path_panel.isVisible() + integration_panel_visibility = self.integration_panel.isVisible() + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + + # check if the installation panel is shown in the installer application + # -> if true: -> set UNICADO install path to browse edit-field of installer application + if install_path_panel_visibility: + install_string = self.install_path_line_edit.text() + install_string = str(install_string.replace(os.sep, '/')) + if len(install_string) < 3: + install_string = 'C:/Programs/UNICADOworkflow' + # check if the current operating system is linux + # -> if true: -> set linux specific install path to browse edit-field + if os.name == 'posix': + install_string = user_path_string + 'UNICADOworkflow' + + # check if the given install path is not an existing directory -> if true: -> create install directory + if not os.path.isdir(install_string): + os.makedirs(install_string) + install_folder = QFileDialog.getExistingDirectory(self.repository_path_panel, + 'Choose Install Directory', install_string) + self.install_path_line_edit.setText(install_folder) + + # check if the current operating system is windows + # -> if true: -> set windows specific install path to browse edit-field + if os.name == 'nt': + check_input_string = install_folder.find('C:/Program Files') + # check if the 'C:/Program Files' is not existing + # -> if true: -> try to generate a test folder inside of current hard drive directory + # to check for necessary administrator rights + if check_input_string == 0: + try: + if os.path.isdir(install_folder): + os.mkdir(install_folder + '/test') + shutil.rmtree(install_folder + '/test') + else: + os.mkdir(install_folder) + self.install_path_text_label_1.setText("ATTENTION: If UNICADOworkflow is to be installed in" + " C:/Program Files or C:/Program Files (x86), " + "the installer and RCE must be executed with " + "administrator rights. Ensure that you have these rights.") + QCoreApplication.processEvents() + + # exception handling if necessary administrator rights not existing + # -> set UNICADO install path to 'C:/Programs/UNICADOworkflow/' + except OSError: + install_folder = 'C:/Programs/UNICADOworkflow/' + self.install_path_text_label_1.setText("ATTENTION: Please note that you do not have the necessary " + "administrator rights for the installation in the selected " + "directory. Installation continues in" + " C:/Programs/UNICADOworkflow/.") + self.install_path_line_edit.setText(install_folder) + QCoreApplication.processEvents() + + # else condition: necessary administrator rights are existing + # -> set install path to given path from browse edit-field + else: + self.install_path_text_label_1.setText("Setup will install the UNICADO workflow in the following " + "folder. To install in a different folder, click Browse and " + "select another folder. Attention, this cannot be changed later!" + " Then click Next to select the repository folder.") + QCoreApplication.processEvents() + if not os.path.isdir(install_folder): + self.install_path_line_edit.setText(install_string) + QCoreApplication.processEvents() + if not (install_folder == install_string): + check_input_string = install_string.find('C:/Program Files') + if check_input_string == -1: + os.removedirs(install_string) + + # check if the current operating system is linux + # -> if true: -> set linux specific install path to browse edit-field + if os.name == 'posix': + if not os.path.isdir(install_folder): + self.install_path_line_edit.setText(install_string) + QCoreApplication.processEvents() + if not (install_folder == install_string): + os.removedirs(install_string) + + # check if the select repository panel is shown in the installer application + # -> if true: -> check if all necessary repositories exist and set git url path + if repository_path_panel_visibility: + current_dir = os.path.expanduser("~") + repository_folder = QFileDialog.getExistingDirectory(self.repository_path_panel, 'Select Repository Directory', + current_dir) + if not os.path.isdir(repository_folder): + self.repository_path_line_edit.setText(current_dir) + QCoreApplication.processEvents() + repository_folder = current_dir + else: + self.repository_path_line_edit.setText(repository_folder) + QCoreApplication.processEvents() + + self.next_button.setEnabled(False) + QCoreApplication.processEvents() + folders_in_directory = os.listdir(repository_folder) + for folder in folders_in_directory: + path_to_check = repository_folder + '/' + folder + '/.git' + if os.path.isdir(path_to_check): + if os.path.isfile(repository_folder + '/' + folder + '/.git/config'): + config = configparser.ConfigParser() + config.read(repository_folder + '/' + folder + '/.git/config') + if config.has_section('remote "origin"'): + url = config['remote "origin"']['url'] + if url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rADDITIONALSOFTWARE.git': + self.next_button.setEnabled(True) + QCoreApplication.processEvents() + break + elif url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rAircraftDesign.git': + self.next_button.setEnabled(True) + QCoreApplication.processEvents() + break + elif url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rAIRCRAFTREFERENCES.git': + self.next_button.setEnabled(True) + QCoreApplication.processEvents() + break + elif url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rEngines.git': + self.next_button.setEnabled(True) + QCoreApplication.processEvents() + break + elif url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rLibraries.git': + self.next_button.setEnabled(True) + QCoreApplication.processEvents() + break + + if integration_panel_visibility: + tool_name = self.tool_name_panel_edit.text() + current_dir = os.path.expanduser("~") + local_tool_folder = QFileDialog.getExistingDirectory(self.tool_path_panel, 'Select Tool Directory', current_dir) + self.tool_path_panel_edit.setText(local_tool_folder) + content_of_directory = os.listdir(local_tool_folder) + if os.path.isdir(local_tool_folder + '/' + tool_name): + self.next_button.setEnabled(False) + self.integration_text_label_4.setVisible(False) + self.integration_text_label_3.setVisible(True) + self.integration_combo_box.setVisible(False) + self.integration_text_label_3.setText( + 'Attention: There is no executable file with the entered tool name in the selected directory. ' + 'Please change path.') + else: + if tool_name in content_of_directory or tool_name + '.exe' in content_of_directory: + self.integration_text_label_3.setVisible(True) + self.integration_combo_box.setVisible(True) + self.integration_text_label_3.setText('Please select the group in which the tool is to be integrated.') + else: + self.next_button.setEnabled(False) + self.integration_combo_box.setVisible(False) + self.integration_text_label_4.setVisible(False) + self.integration_text_label_3.setVisible(True) + self.integration_text_label_3.setText( + 'Attention: There is no executable file with the entered tool name in the selected directory. ' + 'Please change path.') + + QCoreApplication.processEvents() diff --git a/installer/sub_functions/change_execution_rights_on_linux.py b/installer/sub_functions/change_execution_rights_on_linux.py new file mode 100644 index 0000000000000000000000000000000000000000..a17d28fa47289001f006a4766cdfe556c5a7b001 --- /dev/null +++ b/installer/sub_functions/change_execution_rights_on_linux.py @@ -0,0 +1,19 @@ +def change_execution_rights_on_linux(path_to_folder): + """ Changes the access rights of all files inside the given directory to read, write and execute. + + The input string "path_to_folder" contains the system path of directory to be changed. + + :param: path_to_folder: input string + :return: none + """ + + ''' imports for python ''' + import os + import stat + + ''' loop across all directories and files inside the given directory to change access rights ''' + for root, dirs, files in os.walk(path_to_folder): + for directory in dirs: + os.chmod(os.path.join(root, directory), stat.S_IRWXU) + for file in files: + os.chmod(os.path.join(root, file), stat.S_IRWXU) diff --git a/installer/sub_functions/check_tool.py b/installer/sub_functions/check_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..cc4c87d99a1d6b394c509bbfaf2d10799bd042c3 --- /dev/null +++ b/installer/sub_functions/check_tool.py @@ -0,0 +1,179 @@ +import os +from PyQt5.QtCore import QCoreApplication + + +def check_tool(self): + # read tool name from edit fild + input_string = str() + self.integration_text_label_2.setVisible(False) + self.integration_button_group_name.setExclusive(False) + self.integration_radio_button_delete.setChecked(False) + self.integration_radio_button_overwrite.setChecked(False) + self.integration_button_group_name.setExclusive(True) + self.next_button.setEnabled(False) + self.button_name_panel.setVisible(False) + tool_name_panel_visibility = self.tool_name_panel.isVisible() + group_name_panel_visibility = self.group_name_panel.isVisible() + + if tool_name_panel_visibility: + input_string = self.tool_name_panel_edit.text() + + if group_name_panel_visibility: + input_string = self.group_name_panel_edit.text() + + ''' check if the entered tool is already existing in the workflow ''' + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + + # check if the text file with installed tools exist -> if true: + # -> open file and check if the entered tool name already exist + if tool_name_panel_visibility: + if os.path.isfile(user_path_string + '.rce/default/integration/tools/common/toolListUNICADOworkflow.txt'): + with open(user_path_string + '.rce/default/integration/tools/common/toolListUNICADOworkflow.txt', 'r') \ + as tool_list: + if input_string in tool_list.read(): + if os.path.isfile(user_path_string + '.rce/default/integration/tools/common/' + 'standardToolListUNICADOworkflow.txt'): + with open(user_path_string + '.rce/default/integration/tools/common/' + 'standardToolListUNICADOworkflow.txt', 'r') as standard_tools: + if input_string in standard_tools.read(): + self.integration_text_label_2.setText("The entered tool name already exists in the " + "UNICADO workflow" + " and is a basic tool that cannot be changed. \n" + "Please enter another tool name or abort with " + "Cancel.") + self.integration_text_label_2.setVisible(True) + standard_tools.close() + tool_list.close() + + else: + self.integration_text_label_2.setText("The entered tool name already exists in the " + "UNICADO workflow" + " but is not a basic tool. \n" + "Please select if you want to remove or overwrite" + " the tool.") + self.integration_text_label_2.setVisible(True) + self.button_name_panel.setVisible(True) + standard_tools.close() + tool_list.close() + + QCoreApplication.processEvents() + return + + else: + self.integration_text_label_2.setText( + "The entered tool name already exists in the UNICADO workflow" + " but is not a basic tool. \n" + "Please select if you want to remove or overwrite the tool.") + self.integration_text_label_2.setVisible(True) + self.button_name_panel.setVisible(True) + tool_list.close() + + QCoreApplication.processEvents() + return + + tool_list.close() + + # else condition: text file with installed tools is not existing -> abort installer + else: + # generate Error message and show in installer application + self.header_text_label_1.setText('Attention! An Error has occurred!') + self.header_text_label_2.setText('') + self.integration_text_label_1.setText('The UNICADO workflow is not installed correctly! \n' + 'Please reinstall the workflow correctly, after that the tool ' + 'integration' + ' should be possible.') + self.tool_name_panel.setVisible(False) + self.tool_name_panel_edit_button.setVisible(False) + self.next_button.setVisible(False) + self.back_button.setVisible(False) + self.cancel_button.setText('Abort') + return + + ''' check input string for camel case convention ''' + # check if is blank character in the given input string -> if true: -> display an error message in ui + if not input_string.isalnum(): + if tool_name_panel_visibility: + self.integration_text_label_2.setText("Blanks or special characters are not allowed in the tool name, " + "please change.") + self.integration_text_label_2.setVisible(True) + + if group_name_panel_visibility: + self.integration_text_label_4.setText("Blanks or special characters are not allowed in the group name, " + "please change.") + self.integration_text_label_4.setVisible(True) + + QCoreApplication.processEvents() + return + + # check if is a number in the given string -> if true: -> display an error message in ui + if not input_string.isalpha(): + if tool_name_panel_visibility: + self.integration_text_label_2.setText("Numbers are not allowed in the tool name, please change.") + self.integration_text_label_2.setVisible(True) + + if group_name_panel_visibility: + self.integration_text_label_4.setText("Numbers are not allowed in the group name, please change.") + self.integration_text_label_4.setVisible(True) + + QCoreApplication.processEvents() + return + + # check if is the first character upper case -> if true: -> display an error message in ui + if not input_string[0].islower(): + if tool_name_panel_visibility: + self.integration_text_label_2.setText("First letter in tool name is not allowed to be uppercase, " + "please change.") + self.integration_text_label_2.setVisible(True) + + if group_name_panel_visibility: + self.integration_text_label_4.setText("First letter in group name is not allowed to be uppercase, " + "please change.") + self.integration_text_label_4.setVisible(True) + + QCoreApplication.processEvents() + return + + # check if all characters of input string are lower case -> if true: -> display an error message in ui + if input_string.islower(): + if tool_name_panel_visibility: + self.integration_text_label_2.setText("Only lowercase letters are not allowed, please change.") + self.integration_text_label_2.setVisible(True) + + if group_name_panel_visibility: + self.integration_text_label_4.setText("Only lowercase letters are not allowed, please change.") + self.integration_text_label_4.setVisible(True) + + QCoreApplication.processEvents() + return + + # check if there is exactly one uppercase letter in the given string + camel_case_flag = [True for character in input_string if character.isupper()] + upper_character_count = [1 for character in input_string if character.isupper()] + if not camel_case_flag or len(upper_character_count) > 1: + if tool_name_panel_visibility: + self.integration_text_label_2.setText("Only one uppercase letter is allowed in the tool name, " + "please change.") + self.integration_text_label_2.setVisible(True) + + if group_name_panel_visibility: + self.integration_text_label_4.setText("Only one uppercase letter is allowed in the tool name, " + "please change.") + self.integration_text_label_4.setVisible(True) + + QCoreApplication.processEvents() + return + + # the entered tool name matched the camel case convention -> prepare ui for the next steps + if tool_name_panel_visibility: + self.integration_text_label_2.setText("Please click 'Next' to continue the tool integration.") + self.integration_text_label_2.setVisible(True) + + if group_name_panel_visibility: + self.integration_text_label_4.setText("Please click 'Next' to continue the tool integration.") + self.integration_text_label_4.setVisible(True) + + self.next_button.setEnabled(True) + QCoreApplication.processEvents() + return diff --git a/installer/sub_functions/current_text.py b/installer/sub_functions/current_text.py new file mode 100644 index 0000000000000000000000000000000000000000..58ef13c7ba47ab3a0334ff92baad5d02551675b4 --- /dev/null +++ b/installer/sub_functions/current_text.py @@ -0,0 +1,24 @@ +# imports for python +from sub_functions.check_tool import check_tool + + +def current_text(self): + selected_group = self.integration_combo_box.currentText() + + if selected_group == '- please select a tool group -' or selected_group == '- other -': + self.integration_text_label_4.setVisible(False) + self.next_button.setEnabled(False) + self.group_name_panel.setVisible(False) + if selected_group == '- other -': + self.group_name_panel.setVisible(True) + input_string = self.group_name_panel_edit.text() + if not input_string == 'Please enter tool group name.': + check_tool(self) + else: + self.group_name_panel_edit.setText(selected_group) + self.group_name_panel.setVisible(False) + self.integration_text_label_4.setText("Please click 'Next' to continue the tool integration.") + self.integration_text_label_4.setVisible(True) + self.next_button.setEnabled(True) + + return diff --git a/installer/sub_functions/delete_tool.py b/installer/sub_functions/delete_tool.py new file mode 100644 index 0000000000000000000000000000000000000000..ffeed5ba472ab2ac16e9b9d52e9f35a4e5360d6a --- /dev/null +++ b/installer/sub_functions/delete_tool.py @@ -0,0 +1,55 @@ +import os +import time +import shutil + + +def delete_tool(self): + # initialize local parameter + tools = [] + install_directory = [] + tool_name = self.tool_name_panel_edit.text() + error_flag = False + + # generate path to internal rce tool directory + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + path_to_json_files = user_path_string + '.rce/default/integration/tools/common/' + + if not os.path.isfile(path_to_json_files + 'absolutPathToUNICADOInstallDirectory.txt'): + error_flag = True + + else: + install_path = open(user_path_string + '.rce/default/integration/tools/common/' + 'absolutPathToUNICADOInstallDirectory.txt', 'r') + install_directory = install_path.read() + install_path.close() + + if not os.path.isfile(path_to_json_files + 'toolListUNICADOworkflow.txt'): + error_flag = True + + else: + tool_list = open(user_path_string + '.rce/default/integration/tools/common/toolListUNICADOworkflow.txt', 'r') + tools = tool_list.read() + tool_list.close() + + if not error_flag: + if os.path.isdir(path_to_json_files + tool_name): + shutil.rmtree(path_to_json_files + tool_name) + + if tool_name in tools: + tools = tools.split() + tools.remove(tool_name) + tools = '\n'.join([str(item) for item in tools]) + tool_list = open(user_path_string + '.rce/default/integration/tools/common/toolListUNICADOworkflow.txt', + 'w') + tool_list.write(tools) + tool_list.write('\n') + tool_list.close() + + if os.path.isdir(install_directory + tool_name): + shutil.rmtree(install_directory + tool_name) + + time.sleep(3) + + return error_flag diff --git a/installer/sub_functions/install_unicado.py b/installer/sub_functions/install_unicado.py new file mode 100644 index 0000000000000000000000000000000000000000..00357a56ab77d7e7154526e40f186950ade0af54 --- /dev/null +++ b/installer/sub_functions/install_unicado.py @@ -0,0 +1,1360 @@ +''' imports for python ''' +import os +import json +import time +import shutil +import platform +import subprocess +import configparser +from zipfile import ZipFile +from pathlib import Path +from PyQt5.QtCore import QCoreApplication +from sub_functions.resource_path import resource_path +from sub_functions.write_path_to_environment import write_path_to_environment +from sub_functions.change_execution_rights_on_linux import change_execution_rights_on_linux +from sub_functions.remove_tigl_entry import remove_tigl_entry_from_wf_file + +def install_unicado(self): + """ Call function to uninstall all UNICADO components of the working- and .rce-directory. + + rtype: object + """ + + ''' initialize local parameter ''' + percent = int(0) + install_percent = int(0) + current_working_step = str() + python_error_flag = False + error_flag = False + + # set buttons to invisible or disabled + self.next_button.setEnabled(False) + self.back_button.setVisible(False) + self.cancel_button.setVisible(False) + self.cancel_button.setText('Finish') + self.install_progress_bar.setValue(1) + QCoreApplication.processEvents() + + # check if the standalone installation is selected + if self.install_radio_button_alone.isChecked(): + # read UNICADO install path from line edit field of install panel + install_folder = self.install_path_line_edit.text() + install_folder = install_folder.replace(os.sep, '/') + if not os.path.isdir(install_folder): + os.makedirs(install_folder) + + # create path-files for UNICADOworkflow + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + path_to_tool_json_files = user_path_string + '.rce/default/integration/tools/common/' + + # check if the .rce system path for json tool integration is not existing + # -> if true: -> generate directories recursively + if not os.path.isdir(path_to_tool_json_files): + os.makedirs(path_to_tool_json_files) + + # check if a standalone origin design tool directory is not existing in the .rce folder + # -> if true: -> generate directories recursively + if not os.path.isdir(path_to_tool_json_files + 'originUnicadoDesignTools'): + os.makedirs(path_to_tool_json_files + 'originUnicadoDesignTools/UNICADOworkflow') + + # check if a standalone origin software tool directory is not existing in the .rce folder + # -> if true: -> generate directories recursively + if not os.path.isdir(path_to_tool_json_files + 'originUnicadoProjects'): + os.makedirs(path_to_tool_json_files + 'originUnicadoProjects') + + # generate standalone flag file in .rce directory + if not os.path.isfile(path_to_tool_json_files + 'standAloneInstallationFlag.dat'): + with open(path_to_tool_json_files + 'standAloneInstallationFlag.dat', 'w') as file: + file.close() + + # delete repository path file if it is existing + if os.path.isfile(path_to_tool_json_files + 'absolutPathToUNICADOrepositoryDirectory.txt'): + os.remove(path_to_tool_json_files + 'absolutPathToUNICADOrepositoryDirectory.txt') + + # get path to temporary working directory including the version txt-file + path_to_resources, status_flag = resource_path() + path_to_version_file = os.path.join(path_to_resources, 'version.txt').replace(os.sep, '/') + + # check if python is already installed on the local machine + startupinfo = None + python_executables = [] + + try: + # For Windows, use the `where` command + if os.name == "nt": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW # Suppress console window + result = subprocess.run(["where", "python"], capture_output=True, text=True, check=True, + startupinfo=startupinfo) + python_executables = result.stdout.strip().split("\n") + else: + # For Linux/macOS, use the `which -a` command + with open(os.devnull, 'w') as devnull: + result = subprocess.run(["which", "-a", "python"], text=True, check=True, + stdout=subprocess.PIPE, stderr=devnull) + python_executables = result.stdout.strip().split("\n") + except subprocess.CalledProcessError as e: + print(f"Failed to locate Python interpreters. Error: {e}") + + # Filter and clean up results + installed_python_versions = [exe.strip() for exe in python_executables if exe.strip()] + + # check if any python version is installed on the lcoal machine + if installed_python_versions: + python_count = 0 + python_numbers = [] + python_versions = [] + python_check_flag = False + # loop across all installed python verions to extract version number + for py_version in range(0, len(installed_python_versions)): + if not 'INKSCAPE' in installed_python_versions[py_version].upper() and \ + not '.VIRTUALENVS' in installed_python_versions[py_version].upper() and \ + not 'WINDOWSAPPS' in installed_python_versions[py_version].upper(): + try: + # Prepare platform-specific settings to suppress console output + startupinfo = None + if os.name == "nt": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + # Capture Python version silently + with open(os.devnull, 'w') as devnull: + results = subprocess.run([installed_python_versions[py_version], "--version"], + stdout=subprocess.PIPE, stderr=devnull, text=True, check=True, + startupinfo=startupinfo) + # Process and store the version information + python_versions.append(results.stdout.strip()) + python_numbers.append(results.stdout.strip().split(' ')[-1]) + except subprocess.CalledProcessError as e: + print(f"Error retrieving version for {py_version}: {e}") + + # loop across all extracted python verion numbers to check if one of them is valid for UNCIADO + for py_number in range(0, len(python_numbers)): + if int(python_numbers[py_number][0]) == 3 \ + and (int(python_numbers[py_number][2:4]) == 10 or int(python_numbers[py_number][2:4]) == 11): + python_check_flag = True + break + python_count += 1 + + # check if the python version check is failed -> if true: -> set error message and prepare termination + if not python_check_flag: + python_error_flag = True + python_error_string_1 = 'Attention: Your installed Python versions are not valid.' + python_error_string_2 = 'Currently only Python 3.10 and 3.11 are supported.' + + # else conditon: no python installation was found on the local machine + else: + python_error_flag = True + python_error_string_1 = 'Attention! No installed Python version was found on your local machine!' + python_error_string_2 = 'At least one non third party version should be added to the PATH variables of your system.' + + # Read list of python modules + path_to_python_module_file = os.path.join(path_to_resources, 'python_module_list.txt').replace(os.sep, '/') + if os.path.isfile(path_to_python_module_file) and not python_error_flag: + python_module_file = open(path_to_python_module_file, 'r') + python_module_list = python_module_file.read() + python_module_file.close() + + # check if the python version check is not failed -> if true: -> run installation process + if not python_error_flag: + # check if the version text file is really existing -> if true: -> read version number from txt-file + if os.path.isfile(path_to_version_file) and status_flag: + file = open(path_to_version_file, 'r') + version_number = file.read() + file.close() + + # get path to temporary working directory including the installation zip-file + if os.name == 'posix': + name_of_zip = 'UNICADO-' + version_number + '-Linux.zip' + else: + name_of_zip = 'UNICADO-' + version_number + '-win64.zip' + index = path_to_version_file.rfind('/') + path_to_zip = path_to_version_file[:index] + '/' + name_of_zip + path_to_temp = path_to_version_file[:index] + + # check if the installation zip file is really existing + if os.path.isfile(path_to_zip): + # delete old temporary zip directory if it is existing + if os.path.isdir(path_to_temp + '/' + name_of_zip[:-4]): + shutil.rmtree(path_to_temp + '/' + name_of_zip[:-4]) + + # try to unzip installer data an install workflow component + try: + current_working_step = 'unzipping' + with ZipFile(path_to_zip, 'r') as zObject: + # Extracting specific file in the zip into a specific location + zObject.extractall(path=path_to_temp) + zObject.close() + + if os.path.isdir(path_to_temp + '/' + name_of_zip[:-4]): + content_of_zip_directory = os.listdir(path_to_temp + '/' + name_of_zip[:-4]) + copy_count = len(content_of_zip_directory) + 20 + percent = int(round(100 / copy_count, 0)) + + # generate text file for RCE with install directory + current_working_step = 'path generation' + file_for_install_path = open(path_to_tool_json_files + + 'absolutPathToUNICADOInstallDirectory.txt', 'w') + file_for_install_path.write(install_folder + '/') + file_for_install_path.close() + install_percent += percent + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + # generate results and working directory + current_working_step = 'tool list generation' + if not install_folder[-1] == '/': + install_folder = install_folder + '/' + if not os.path.isdir(install_folder + 'workflowResults'): + os.mkdir(install_folder + 'workflowResults') + if not os.path.isdir(install_folder + 'workingDirectoryRCE'): + os.mkdir(install_folder + 'workingDirectoryRCE') + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + file_for_tool_list.write('workflowResults' + '\n') + file_for_tool_list.write('workingDirectoryRCE' + '\n') + file_for_tool_list.close() + + # loop across all elements from temporary zip directory to install UNICADO + for content in content_of_zip_directory: + current_working_step = 'installation of ' + content + # open text file to write tool list + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + path_of_content = path_to_temp + '/' + name_of_zip[:-4] + '/' + content + if os.path.isdir(path_of_content): + if not content == 'workflowComponent': + # add tool name to tool list file + file_for_tool_list.write(content + '\n') + file_for_tool_list.close() + # copy design tool to install directory + src_path = Path(path_of_content).resolve() + dst_path = Path(install_folder + content).resolve() + if os.name == "nt": + shutil.copytree(r"\\?\{}".format(src_path), r"\\?\{}".format(dst_path)) + else: + shutil.copytree(src_path, dst_path) + + # copy design tool to origin design tool directory + if not content == 'projects': + src_path = Path(path_of_content).resolve() + dst_path = Path(path_to_tool_json_files + 'originUnicadoDesignTools/' + content).resolve() + if os.name == "nt": + shutil.copytree(r"\\?\{}".format(src_path), r"\\?\{}".format(dst_path)) + else: + shutil.copytree(src_path, dst_path) + + # copy aircraft projects to origin projects directory + else: + src_path = Path(path_of_content).resolve() + dst_path = Path(path_to_tool_json_files + 'originUnicadoProjects/' + content).resolve() + if os.name == "nt": + shutil.copytree(r"\\?\{}".format(src_path), r"\\?\{}".format(dst_path)) + else: + shutil.copytree(src_path, dst_path) + + # copy tool json file to hidden .rce directory + if not content == 'projects' and not content == 'unicadoRuntimeLibs': + path_json_file = path_to_temp + '/' + name_of_zip[:-4] \ + + '/workflowComponent/jsonFiles/' + content + # check if a sjson file of current tool exist + if os.path.isdir(path_json_file): + src_path = Path(path_json_file).resolve() + dst_path = Path(path_to_tool_json_files + '/' + content).resolve() + if os.name == "nt": + shutil.copytree(r"\\?\{}".format(src_path), r"\\?\{}".format(dst_path)) + else: + shutil.copytree(src_path, dst_path) + # write paths to json configuration file of each module + with open(path_to_tool_json_files + content + '/configuration.json', + 'r+') as jsonFile: + json_data = json.load(jsonFile) + launch_settings = json_data['launchSettings'] + launch_settings[0]['rootWorkingDirectory'] =\ + install_folder + 'workingDirectoryRCE' + launch_settings[0]['toolDirectory'] = install_folder + content + if content in python_module_list: + if os.name == 'posix': + commandScriptLinux = json_data['commandScriptLinux'] + json_data['commandScriptLinux'] = \ + (installed_python_versions[python_count] + ' ' + commandScriptLinux) + else: + commandScriptWindows = json_data['commandScriptWindows'] + json_data['commandScriptWindows'] = \ + (installed_python_versions[python_count] + ' ' + commandScriptWindows) + json.dump(json_data, open(path_to_tool_json_files + content + + '/configuration.json', 'w'), + indent=2, sort_keys=False) + + # update ui + install_percent += percent + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + # installation of standalone workflow components + elif content == 'workflowComponent': + current_working_step = 'installation of ' + content + if not os.path.isdir(path_of_content + '/UNICADOworkflow'): + os.makedirs(path_of_content + '/UNICADOworkflow') + + # copy workflow file to istallation directory + src_path = Path(path_of_content + '/UNICADOworkflow').resolve() + dst_path = Path(install_folder + '/workingDirectoryRCE/UNICADOworkflow').resolve() + if os.name == "nt": + shutil.copytree(r"\\?\{}".format(src_path), r"\\?\{}".format(dst_path)) + else: + shutil.copytree(src_path, dst_path) + + workflow_file_path = \ + install_folder + '/workingDirectoryRCE/UNICADOworkflow/UNICADOworkflow.wf' + if os.path.isfile(workflow_file_path): + with open(workflow_file_path, "r") as workflow_file: + json_data = workflow_file.read() # Read the entire file as a string + + # Replace the placeholder with the desired value + print(installed_python_versions[python_count].replace(os.sep, '/')) + updated_json_data = \ + json_data.replace('${pythonExecutionPath}', + installed_python_versions[python_count].replace(os.sep, '/')) + + # Validate the JSON to ensure it's still valid + try: + data = json.loads(updated_json_data) # Parse the updated JSON + print("JSON is valid. Writing updates back to the same file...") + + # Write the updated JSON back to the same file + with open(workflow_file_path, "w") as file: + file.write(updated_json_data) + + # create back-up file of workflow + if os.path.isfile(install_folder + '/workingDirectoryRCE/UNICADOworkflow/UNICADOworkflow_backUp.wf'): + os.remove(install_folder + '/workingDirectoryRCE/UNICADOworkflow/UNICADOworkflow_backUp.wf') + + # copy workflow back-up file to istallation directory + src_path = Path(workflow_file_path).resolve() + dst_path = Path(install_folder + '/workingDirectoryRCE/UNICADOworkflow/UNICADOworkflow_backUp.wf').resolve() + if os.name == "nt": + shutil.copyfile(r"\\?\{}".format(src_path), r"\\?\{}".format(dst_path)) + else: + shutil.copyfile(src_path, dst_path) + print(f"Updated JSON has been written to {workflow_file_path}") + except json.JSONDecodeError as e: + print(f"Error: The JSON structure is invalid after replacement. {e}") + + # copy json files to origin design tool directory + if not os.path.isdir(path_to_tool_json_files + + 'originUnicadoDesignTools/UNICADOworkflow/jsonFiles'): + os.makedirs(path_to_tool_json_files + + 'originUnicadoDesignTools/UNICADOworkflow/jsonFiles') + + list_of_json_files = os.listdir(path_of_content + '/jsonFiles') + for json_file in list_of_json_files: + # copy module json files to hidden .rce directory + src_path = Path(path_of_content + '/jsonFiles/' + json_file).resolve() + dst_path = Path(path_to_tool_json_files + + 'originUnicadoDesignTools/UNICADOworkflow/jsonFiles/' + + json_file).resolve() + if os.name == "nt": + shutil.copytree(r"\\?\{}".format(src_path), r"\\?\{}".format(dst_path)) + else: + shutil.copytree(src_path, dst_path) + + # update ui + install_percent += percent + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + # check if the current operating system a linux os -> if true: -> Remove TiGL Viewer and change execution rights + if os.name == 'posix': + # call function to remove TiGL viewer workflow component and its connections within the workflow, since deprecated on linux + remove_tigl_entry_from_wf_file(install_folder + 'workingDirectoryRCE/UNICADOworkflow/UNICADOworkflow.wf') + # call function to change the execution rights on linux os of each module + change_execution_rights_on_linux(install_folder) + + # else condition: unzipping of installer data failed -> display error message and abort installation + else: + error_flag = True + self.welcome_panel.setVisible(True) + self.integration_panel.setVisible(False) + self.header_panel.setVisible(False) + self.welcome_text_label_1.setText('Attention! An internal error has occurred!') + self.install_text_label_1.setText('Unzipping of installation components failed!') + self.install_text_label_2.setText('Please reinstall UNICADOworkflow or contact the' + ' developer team.') + self.continue_text_label.setText('To close the installer, click Abort.') + self.update_button.setVisible(False) + self.update_button.setEnabled(True) + self.cancel_button.setText('Abort') + self.cancel_button.setEnabled(True) + self.cancel_button.setVisible(True) + self.next_button.setVisible(False) + QCoreApplication.processEvents() + + # delete temporary files from system after successfully installation + if os.path.isdir(path_to_temp + '/' + name_of_zip[:-4]): + shutil.rmtree(path_to_temp + '/' + name_of_zip[:-4]) + # update ui + install_percent += percent + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + except OSError: + error_flag = True + self.welcome_panel.setVisible(True) + self.integration_panel.setVisible(False) + self.header_panel.setVisible(False) + self.welcome_text_label_1.setText('Attention! An internal error has occurred!') + self.install_text_label_1.setText('Error during ' + current_working_step + '!') + self.install_text_label_2.setText('Please reinstall UNICADOworkflow or contact the developer team.') + self.continue_text_label.setText('To close the installer, click Abort.') + self.update_button.setVisible(False) + self.update_button.setEnabled(True) + self.cancel_button.setText('Abort') + self.cancel_button.setEnabled(True) + self.cancel_button.setVisible(True) + self.next_button.setVisible(False) + QCoreApplication.processEvents() + + # Try to install python required packages + if not error_flag: + if os.path.isdir(install_folder + '/lib'): + # Try to install own unicado python packages + try: + own_package_list = \ + open(os.path.join(path_to_resources, + 'own_python_packages.txt').replace(os.sep, '/'), 'r') + for own_package in own_package_list: + clean_position = own_package.find('\n') + if clean_position != -1: + own_package = own_package[:clean_position] + if os.path.isdir(install_folder + 'lib/' + own_package): + # Suppress output using devnull and platform-specific logic + with open(os.devnull, 'w') as devnull: + if os.name == "nt": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + subprocess.check_call([installed_python_versions[python_count], + "-m", "pip", "install", "-e", "."], + cwd=install_folder + 'lib/' + own_package, + stdout=devnull, stderr=devnull, + startupinfo=startupinfo) + else: # For Linux/macOS + subprocess.check_call( + [installed_python_versions[python_count], + "-m", "pip", "install", "-e", "."], + cwd=install_folder + 'lib/' + own_package, + stdout=devnull, stderr=devnull) + + # Update UI + install_percent += percent + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + # Copy package list file and store Python path + src_path = Path(os.path.join(path_to_resources, 'own_python_packages.txt').replace(os.sep, '/')).resolve() + dst_path = Path(path_to_tool_json_files + 'unicado_own_python_packages.txt').resolve() + if os.name == "nt": + shutil.copyfile(r"\\?\{}".format(src_path), r"\\?\{}".format(dst_path)) + else: + shutil.copyfile(src_path, dst_path) + with open(path_to_tool_json_files + 'unicado_python_path.txt', 'w') as python_path_file: + python_path_file.write(installed_python_versions[python_count]) + + except OSError: + print('Error: The file "own_python_packages.txt" could not be opened!') + else: + print('Attention: The lib directory could not be found inside of the UNICADO installation.\n' + ' Required own Python packages could not be installed.') + + # Try to install required standard python packages + try: + standard_package_list = \ + open(os.path.join(path_to_resources, + 'standard_python_packages.txt').replace(os.sep, '/'), 'r') + for standard_python_package in standard_package_list: + clean_position = standard_python_package.find('\n') + if clean_position != -1: + standard_python_package = standard_python_package[:clean_position] + with open(os.devnull, 'w') as devnull: + if os.name == "nt": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + subprocess.check_call([installed_python_versions[python_count], + "-m", "pip", "install", standard_python_package], + stdout=devnull, stderr=devnull, + startupinfo=startupinfo) + else: # For Linux/macOS + subprocess.check_call( + [installed_python_versions[python_count], + "-m", "pip", "install", standard_python_package], + stdout=devnull, stderr=devnull) + + # Update UI + install_percent += percent + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + except OSError: + print('Error: The file "standard_python_packages.txt" could not be opened!') + + # else condition: installer zip file not found -> display error message and abort installation + else: + error_flag = True + self.welcome_panel.setVisible(True) + self.integration_panel.setVisible(False) + self.header_panel.setVisible(False) + self.welcome_text_label_1.setText('Attention! An internal error has occurred!') + self.install_text_label_1.setText('Standalone installation packages not found!') + self.install_text_label_2.setText('Please reinstall UNICADOworkflow or contact the developer team.') + self.continue_text_label.setText('To close the installer, click Abort.') + self.update_button.setVisible(False) + self.update_button.setEnabled(True) + self.cancel_button.setText('Abort') + self.cancel_button.setEnabled(True) + self.cancel_button.setVisible(True) + self.next_button.setVisible(False) + QCoreApplication.processEvents() + + # else condition: version file not found -> display error message and abort installation + else: + error_flag = True + self.welcome_panel.setVisible(True) + self.integration_panel.setVisible(False) + self.header_panel.setVisible(False) + self.welcome_text_label_1.setText('Attention! An internal error has occurred!') + if not status_flag: + self.install_text_label_1.setText('At least two installer application are currently running!') + self.install_text_label_2.setText('Please wait until all other installations are completed ' + 'and try again.') + else: + self.install_text_label_1.setText('Version file not found!') + self.install_text_label_2.setText('Please reinstall UNICADOworkflow or contact the developer team.') + self.continue_text_label.setText('To close the installer, click Abort.') + self.update_button.setVisible(False) + self.update_button.setEnabled(True) + self.cancel_button.setText('Abort') + self.cancel_button.setEnabled(True) + self.cancel_button.setVisible(True) + self.next_button.setVisible(False) + QCoreApplication.processEvents() + + # Python check is failed -> display error message and abort installation + else: + error_flag = True + self.welcome_panel.setVisible(True) + self.integration_panel.setVisible(False) + self.header_panel.setVisible(False) + self.welcome_text_label_1.setText('Attention! An internal error has occurred!') + if not status_flag: + self.install_text_label_1.setText(python_error_string_1) + self.install_text_label_2.setText(python_error_string_2) + else: + self.install_text_label_1.setText(python_error_string_1) + self.install_text_label_2.setText(python_error_string_2) + self.continue_text_label.setText('To close the installer, click Abort.') + self.update_button.setVisible(False) + self.update_button.setEnabled(True) + self.cancel_button.setText('Abort') + self.cancel_button.setEnabled(True) + self.cancel_button.setVisible(True) + self.next_button.setVisible(False) + QCoreApplication.processEvents() + + # else condition: the repository installation is selected + else: + ''' initialize local parameter ''' + path_to_software_tools = str() + path_to_aircraft_projects = str() + path_to_aircraft_design_tools = str() + + # read UNICADO install path from line edit field of install panel + install_folder = self.install_path_line_edit.text() + install_folder = install_folder.replace(os.sep, '/') + last_character = install_folder[-1] + # check if the last character of install path is not '/' + # -> if true: -> add '/' to the end of path of install folder + if not (last_character == '/'): + install_folder = install_folder + '/' + path_of_origin_tool_directory = self.repository_path_line_edit.text() + path_of_origin_tool_directory = path_of_origin_tool_directory.replace(os.sep, '/') + last_character = path_of_origin_tool_directory[-1] + + # check if the last character of repository path is not '/' + # -> if true: -> add '/' to the end of path of repositories + if not (last_character == '/'): + path_of_origin_tool_directory = path_of_origin_tool_directory + '/' + + # create path-files for UNICADOworkflow + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + path_to_tool_json_files = user_path_string + '.rce/default/integration/tools/common/' + + # check if the .rce system path for json tool integration is not existing + # -> if true: -> generate directories recursively + if not os.path.isdir(path_to_tool_json_files): + os.makedirs(path_to_tool_json_files) + + # read repository paths from .git configuration files and set repository paths to copy files + files = os.listdir(path_of_origin_tool_directory) + for fileName in files: + check_if_file_is_directory = os.path.isdir(path_of_origin_tool_directory + fileName) + if check_if_file_is_directory: + check_git_folder = os.path.isdir(path_of_origin_tool_directory + fileName + '/.git') + if check_git_folder: + check_config_file = os.path.isfile(path_of_origin_tool_directory + fileName + '/.git/config') + if check_config_file: + config = configparser.ConfigParser() + config.read(path_of_origin_tool_directory + fileName + '/.git/config') + if config.has_section('remote "origin"'): + url = config['remote "origin"']['url'] + if url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rADDITIONALSOFTWARE.git': + path_to_software_tools = path_of_origin_tool_directory + fileName + '/' + + if url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rAircraftDesign.git': + path_to_aircraft_design_tools = path_of_origin_tool_directory + fileName + '/' + + if url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rAIRCRAFTREFERENCES.git': + path_to_aircraft_projects = path_of_origin_tool_directory + fileName + '/' + + # create list of tools from origin tool directory + files_in_origin_tool_directory = os.listdir(path_to_aircraft_design_tools) + copy_count = len(files_in_origin_tool_directory) + percent = int(round(100 / copy_count, 0)) + + # generate text file for RCE with install directory + file_for_install_path = open(path_to_tool_json_files + 'absolutPathToUNICADOInstallDirectory.txt', 'w') + file_for_install_path.write(install_folder) + file_for_install_path.close() + install_percent += percent + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + # delete standalone flag file if it is existing + if os.path.isfile(path_to_tool_json_files + 'standAloneInstallationFlag.dat'): + os.remove(path_to_tool_json_files + 'standAloneInstallationFlag.dat') + + # generate text file for RCE with repository directory + file_for_repository_path = open(path_to_tool_json_files + 'absolutPathToUNICADOrepositoryDirectory.txt', 'w') + file_for_repository_path.write(path_of_origin_tool_directory) + file_for_repository_path.close() + install_percent += percent + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + time.sleep(1) + + # generate working directory for RCE + if not os.path.isdir(install_folder + 'workingDirectoryRCE'): + os.makedirs(install_folder + 'workingDirectoryRCE/UNICADOworkflow') + os.mkdir(install_folder + 'workingDirectoryRCE/temporaryResults') + + # copy workflow files to working directory + files_to_copy_for_workflow = os.listdir(path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkFlowOnRCE') + for file in files_to_copy_for_workflow: + if os.path.isdir(path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkFlowOnRCE/' + file): + source = path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkFlowOnRCE/' + file + destination = install_folder + 'workingDirectoryRCE/UNICADOworkflow/' + file + shutil.copytree(source, destination) + if os.path.isfile(path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkFlowOnRCE/' + file): + source = path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkFlowOnRCE/' + file + destination = install_folder + 'workingDirectoryRCE/UNICADOworkflow/' + file + shutil.copyfile(source, destination) + QCoreApplication.processEvents() + + shutil.copyfile(path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkflow_conf.xml', + install_folder + 'workingDirectoryRCE/UNICADOworkflow/UNICADOworkflow_conf.xml') + shutil.copyfile(path_to_aircraft_design_tools + 'UNICADOworkflow/version.txt', + install_folder + 'workingDirectoryRCE/UNICADOworkflow/workingVersion.txt') + install_percent += percent + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + file_for_tool_list.write('workingDirectoryRCE' + '\n') + file_for_tool_list.close() + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + if os.name == 'posix': + remove_tigl_entry_from_wf_file(install_folder + 'workingDirectoryRCE/UNICADOworkflow/UNICADOworkflow.wf') + + # generate directory for workflow results + check_flag_workflow_results = os.path.isdir(install_folder + 'workflowResults') + if not check_flag_workflow_results: + os.mkdir(install_folder + '/workflowResults') + install_percent += percent + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + file_for_tool_list.write('workflowResults' + '\n') + file_for_tool_list.close() + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + # for all modules in repositories, copy to install directory + test_list_json_files = os.listdir(path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles/') + for fileName in test_list_json_files: # files_in_origin_tool_directory: + if not (fileName == '.git') and not (fileName == 'template') and not (fileName == 'UNICADOworkflow') \ + and not (fileName == 'convergenceLoop'): + # check if fileName a directory + check_for_directory = os.path.isdir(path_to_aircraft_design_tools + fileName) + check_for_software_tool = os.path.isdir(path_to_software_tools + fileName) + check_existing_json = os.path.isdir(path_to_aircraft_design_tools + + 'UNICADOworkflow/jsonFiles/' + fileName) + # if true -> copy design tool to workingDirectory + if check_for_directory and check_existing_json: + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + file_name_content = os.listdir(path_to_aircraft_design_tools + fileName) + os.mkdir(install_folder + fileName) + for content in file_name_content: + # check if content is a directory -> if true: -> check if content is not equal to 'src' + # -> if true: -> copy content to install folder of current module + if os.path.isdir(path_to_aircraft_design_tools + fileName + '/' + content): + # check if data is not equal to src + # -> if true: -> copy data from origin to working directory + if not content == 'src': + shutil.copytree(path_to_aircraft_design_tools + fileName + '/' + content, + install_folder + fileName + '/' + content) + + # else condition: content is a file -> copy content to install folder of current module + else: + shutil.copyfile(path_to_aircraft_design_tools + fileName + '/' + content, + install_folder + fileName + '/' + content) + + # check if the json file of current module is not existing in .rce directory + # -> if true: -> copy json data to .rce directory + if not os.path.isdir(path_to_tool_json_files + fileName): + shutil.copytree(path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles/' + fileName, + path_to_tool_json_files + fileName) + # else condition: json file of current module is existing in .rce directory + # -> replace old files by json files of current module + else: + shutil.rmtree(path_to_tool_json_files + fileName) + shutil.copytree(path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles/' + fileName, + path_to_tool_json_files + fileName) + + # write tool name to tool list and close file + file_for_tool_list.write(fileName + '\n') + file_for_tool_list.close() + + # write paths to json configuration file of each module + with open(path_to_tool_json_files + fileName + '/configuration.json', 'r+') as jsonFile: + json_data = json.load(jsonFile) + launch_settings = json_data['launchSettings'] + launch_settings[0]['rootWorkingDirectory'] = install_folder + 'workingDirectoryRCE' + launch_settings[0]['toolDirectory'] = install_folder + fileName + json.dump(json_data, open(path_to_tool_json_files + fileName + '/configuration.json', 'w'), + indent=2, sort_keys=False) + # update progress bar + install_percent += percent + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + # if true -> copy software tool to workingDirectory + # -> cpacsInterface is handled separately in the following lines of this function + if check_for_software_tool and check_existing_json and not (fileName == 'cpacsInterface'): + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + shutil.copytree(path_to_software_tools + fileName, install_folder + fileName) + check_tool_file = os.path.isdir(path_to_tool_json_files + fileName) + if not check_tool_file: + shutil.copytree(path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles/' + fileName, + path_to_tool_json_files + fileName) + else: + shutil.rmtree(path_to_tool_json_files + fileName) + shutil.copytree(path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles/' + fileName, + path_to_tool_json_files + fileName) + file_for_tool_list.write(fileName + '\n') + # write paths to json configuration file of each module + with open(path_to_tool_json_files + fileName + '/configuration.json', 'r+') as jsonFile: + json_data = json.load(jsonFile) + launch_settings = json_data['launchSettings'] + launch_settings[0]['rootWorkingDirectory'] = install_folder + 'workingDirectoryRCE' + launch_settings[0]['toolDirectory'] = install_folder + fileName + json.dump(json_data, open(path_to_tool_json_files + fileName + '/configuration.json', 'w'), + indent=2, sort_keys=False) + # update progress bar + install_percent += percent + self.install_progress_bar.setValue(install_percent) + file_for_tool_list.close() + QCoreApplication.processEvents() + + # copy project folder to install directory + files_in_origin_project_directory = os.listdir(path_to_aircraft_projects) + for fileName in files_in_origin_project_directory: + if not (fileName == '.git'): + # check if fileName a directory + check_for_directory = os.path.isdir(path_to_aircraft_projects + fileName) + if check_for_directory: + shutil.copytree(path_to_aircraft_projects + fileName, install_folder + 'projects/' + fileName) + # copy CPACS exchange file to all aircraft projects + if os.path.isfile(path_to_software_tools + + 'cpacsInterface/_zeroFiles/Cpacs_aircraft/Cpacs_aircraft.xml'): + shutil.copyfile(path_to_software_tools + + 'cpacsInterface/_zeroFiles/Cpacs_aircraft/Cpacs_aircraft.xml', + install_folder + 'projects/' + fileName + '/' + fileName + '_CPACS.xml') + + # update progress bar + install_percent += percent + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + file_for_tool_list.write('projects' + '\n') + file_for_tool_list.close() + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + # copy inkscape and gnuplot to working directory + # check for Windows OS + if os.name == 'nt': + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + if not os.path.isdir(install_folder + 'inkscape'): + shutil.copytree(path_to_software_tools + 'inkscape', install_folder + 'inkscape') + file_for_tool_list.write('inkscape' + '\n') + if not os.path.isdir(install_folder + 'gnuplot'): + shutil.copytree(path_to_software_tools + 'gnuplot', install_folder + 'gnuplot') + file_for_tool_list.write('gnuplot' + '\n') + # update progress bar + install_percent += percent + file_for_tool_list.close() + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + # check for linux OS + if os.name == 'posix': + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + if not os.path.isdir(install_folder + 'inkscape-linux'): + shutil.copytree(path_to_software_tools + 'inkscape-linux', install_folder + 'inkscape-linux') + file_for_tool_list.write('inkscape-linux' + '\n') + if not os.path.isdir(install_folder + 'gnuplot-linux'): + shutil.copytree(path_to_software_tools + 'gnuplot-linux', install_folder + 'gnuplot-linux') + file_for_tool_list.write('gnuplot-linux' + '\n') + # update progress bar + install_percent += percent + file_for_tool_list.close() + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + # copy cpacsInterface to working directory + if not os.path.isdir(install_folder + 'cpacsInterface'): + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + file_name_content = os.listdir(path_to_software_tools + 'cpacsInterface') + os.mkdir(install_folder + 'cpacsInterface') + for data in file_name_content: + if os.path.isfile(path_to_software_tools + 'cpacsInterface/' + data): + shutil.copyfile(path_to_software_tools + 'cpacsInterface/' + data, + install_folder + 'cpacsInterface/' + data) + # check if data a directory, then replace with directory from origin tool directory + if os.path.isdir(path_to_software_tools + 'cpacsInterface/' + data): + # check if data is not equal to src or convertUNICADO2CPACS + # -> if true: -> copy data from origin to working directory + if not data == 'src' and not data == 'convertUNICADO2CPACS': + shutil.copytree(path_to_software_tools + 'cpacsInterface/' + data, + install_folder + 'cpacsInterface/' + data) + # check if data is equal to convertUNICADO2CPACS + # -> if true: -> copy all content inside the origin directory out of the src directory + # to working copy + if data == 'convertUNICADO2CPACS': + data_inside = os.listdir(path_to_software_tools + 'cpacsInterface/' + data) + os.mkdir(install_folder + 'cpacsInterface/' + data) + for element in data_inside: + if not element == 'src': + if os.path.isfile(path_to_software_tools + 'cpacsInterface/' + data + '/' + element): + shutil.copyfile(path_to_software_tools + 'cpacsInterface/' + data + '/' + element, + install_folder + 'cpacsInterface/' + data + '/' + element) + else: + shutil.copytree(path_to_software_tools + 'cpacsInterface/' + data + '/' + element, + install_folder + 'cpacsInterface/' + data + '/' + element) + + # check if the json file of cpacsInterface is not existing in the .rce directory + # -> if true: -> copy file to .rce directory + check_tool_file = os.path.isdir(path_to_tool_json_files + 'cpacsInterface') + if not check_tool_file: + shutil.copytree(path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles/cpacsInterface', + path_to_tool_json_files + 'cpacsInterface') + + # else condition: json file of cpacsInterface is existing + # -> delete old file and copy current version to .rce directory + else: + shutil.rmtree(path_to_tool_json_files + 'cpacsInterface') + shutil.copytree(path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles/cpacsInterface', + path_to_tool_json_files + 'cpacsInterface') + # write paths to json configuration file of each module + with open(path_to_tool_json_files + 'cpacsInterface' + '/configuration.json', 'r+') as jsonFile: + json_data = json.load(jsonFile) + launch_settings = json_data['launchSettings'] + launch_settings[0]['rootWorkingDirectory'] = install_folder + 'workingDirectoryRCE' + launch_settings[0]['toolDirectory'] = install_folder + 'cpacsInterface' + json.dump(json_data, open(path_to_tool_json_files + '/cpacsInterface' + '/configuration.json', 'w'), + indent=2, sort_keys=False) + # update progress bar + install_percent += percent + file_for_tool_list.write('cpacsInterface' + '\n') + file_for_tool_list.close() + self.install_progress_bar.setValue(install_percent) + QCoreApplication.processEvents() + + if not error_flag: + # check if the current operating system is windows -> if true: -> check if the installation directory is empty + # -> if true: -> delete install directory + if os.name == 'nt': + if os.path.isdir('C:/Programs/UNICADOworkflow'): + if not os.listdir('C:/Programs/UNICADOworkflow'): + os.rmdir('C:/Programs/UNICADOworkflow') + + # check if the parent directory of install directory is empty -> if true: -> delete parent directory + if not os.listdir('C:/Programs'): + os.rmdir('C:/Programs') + + # check if the current operating system is linux -> if true: -> check if the installation directory is empty + # -> if true: -> delete install directory + if os.name == 'posix': + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + if os.path.isdir(user_path_string + 'UNICADOworkflow'): + if not os.listdir(user_path_string + 'UNICADOworkflow'): + os.rmdir(user_path_string + 'UNICADOworkflow') + + # write absolute paths to json configuration file of each module + path_to_settings_file = user_path_string + '.rce/default/internal/' + if not os.path.isdir(user_path_string + '.rce/default/internal/'): + os.makedirs(user_path_string + '.rce/default/internal/') + + if not os.path.isfile(user_path_string + '.rce/default/internal/settings.json'): + json_data = {'rce.workspace.recentLocations': '', + 'rce.workspace.lastLocation': '', + 'rce.workspace.dontAskAgain': 'false'} + # Write JSON file + with open(user_path_string + '.rce/default/internal/settings.json', 'w') as jsonFile: + json.dump(json_data, open(path_to_settings_file + 'settings.json', 'w')) + + if os.path.isfile(user_path_string + '.rce/default/internal/settings.json'): + with open(path_to_settings_file + 'settings.json', 'r+') as jsonFile: + # check if settings.json file is empty + try: + json_data = json.load(jsonFile) + except OSError: + json_data = {'rce.workspace.recentLocations': '', + 'rce.workspace.lastLocation': '', + 'rce.workspace.dontAskAgain': 'false'} + workspace_location_rce = json_data['rce.workspace.recentLocations'] + + # check if the current operating system is windows + # -> if true: -> set windows specific path to workingDirectoryRCE as default path to RCE + if os.name == 'nt': + install_path = install_folder.replace('/', '\\') + alternate_path = install_path.replace('\\', '\\\\') + json_data['rce.workspace.lastLocation'] = install_path + 'workingDirectoryRCE' + if not install_path[0] + '\:' + '\\' + install_path[3:] + 'workingDirectoryRCE' in workspace_location_rce: + if not install_path[0] + '\:' + alternate_path[2:] + 'workingDirectoryRCE' \ + in workspace_location_rce: + json_data['rce.workspace.recentLocations'] = install_path[0] + '\:' + alternate_path[2:] \ + + 'workingDirectoryRCE' + ':' \ + + workspace_location_rce + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + else: + str_start = workspace_location_rce.index( + install_path[0] + '\:' + alternate_path[2:] + 'workingDirectoryRCE') + str_end = workspace_location_rce.index('workingDirectoryRCE') + if str_start > 0: + sub_string_front = workspace_location_rce[0:str_start - 1] + total_length = len(workspace_location_rce) + new_string = workspace_location_rce[str_start:str_end + 19] + ':' + sub_string_front + if str_end + 19 < total_length: + sub_string_end = workspace_location_rce[str_end + 20:total_length] + new_string = new_string + ':' + sub_string_end + json_data['rce.workspace.recentLocations'] = new_string + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # check if the current operating system is linux + # -> if true: -> set linux specific path to workingDirectoryRCE as default path to RCE + if os.name == 'posix': + json_data['rce.workspace.lastLocation'] = install_folder + 'workingDirectoryRCE' + if not install_folder + 'workingDirectoryRCE' in workspace_location_rce: + json_data['rce.workspace.recentLocations'] = install_folder + 'workingDirectoryRCE' + ':' \ + + workspace_location_rce + QCoreApplication.processEvents() + + # check if rce.workspace.dontAskAgain exist and set to false + if 'rce.workspace.dontAskAgain' in json_data: + json_data['rce.workspace.dontAskAgain'] = 'false' + else: + json_data['rce.workspace.dontAskAgain'] = 'false' + + json.dump(json_data, open(path_to_settings_file + 'settings.json', 'w')) + + # update install process bar and set value to 100% + self.install_progress_bar.setValue(100) + self.next_button.setVisible(False) + self.cancel_button.setVisible(True) + QCoreApplication.processEvents() + + else: + if os.path.isfile(path_to_tool_json_files + 'standAloneInstallationFlag.dat'): + os.remove(path_to_tool_json_files + 'standAloneInstallationFlag.dat') + +def install_unicado_headless(install_dir: Path): + error_flag = False + # read UNICADO install path from line edit field of install panel + install_folder = install_dir + install_folder = str(install_folder.absolute()).replace(os.sep, '/') + if not os.path.isdir(install_folder): + os.makedirs(install_folder) + + # create path-files for UNICADOworkflow + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + path_to_tool_json_files = user_path_string + '.rce/default/integration/tools/common/' + + # check if the .rce system path for json tool integration is not existing + # -> if true: -> generate directories recursively + if not os.path.isdir(path_to_tool_json_files): + os.makedirs(path_to_tool_json_files) + + # check if a standalone origin design tool directory is not existing in the .rce folder + # -> if true: -> generate directories recursively + if not os.path.isdir(path_to_tool_json_files + 'originUnicadoDesignTools'): + os.makedirs(path_to_tool_json_files + 'originUnicadoDesignTools/UNICADOworkflow') + + # check if a standalone origin software tool directory is not existing in the .rce folder + # -> if true: -> generate directories recursively + if not os.path.isdir(path_to_tool_json_files + 'originUnicadoSoftwareTools'): + os.makedirs(path_to_tool_json_files + 'originUnicadoSoftwareTools') + + # check if a standalone origin software tool directory is not existing in the .rce folder + # -> if true: -> generate directories recursively + if not os.path.isdir(path_to_tool_json_files + 'originUnicadoProjects'): + os.makedirs(path_to_tool_json_files + 'originUnicadoProjects') + + # generate standalone flag file in .rce directory + if not os.path.isfile(path_to_tool_json_files + 'standAloneInstallationFlag.dat'): + with open(path_to_tool_json_files + 'standAloneInstallationFlag.dat', 'w') as file: + file.close() + + # delete repository path file if it is existing + if os.path.isfile(path_to_tool_json_files + 'absolutPathToUNICADOrepositoryDirectory.txt'): + os.remove(path_to_tool_json_files + 'absolutPathToUNICADOrepositoryDirectory.txt') + + # get path to temporary working directory including the version txt-file + version_file = 'version.txt' + path_to_version_file, status_flag = resource_path(version_file) + + # check if the version text file is really existing -> if true: -> read version number from txt-file + if os.path.isfile(path_to_version_file) and status_flag: + file = open(path_to_version_file, 'r') + version_number = file.read() + file.close() + + # get path to temporary working directory including the installation zip-file + if os.name == 'posix': + name_of_zip = 'UNICADO-' + version_number + '-Linux.zip' + else: + name_of_zip = 'UNICADO-' + version_number + '-win64.zip' + index = path_to_version_file.rfind('/') + path_to_zip = path_to_version_file[:index] + '/' + name_of_zip + path_to_temp = path_to_version_file[:index] + + # check if the installation zip file is really existing + if os.path.isfile(path_to_zip): + # delete old temporary zip directory if it is existing + if os.path.isdir(path_to_temp + '/' + name_of_zip[:-4]): + shutil.rmtree(path_to_temp + '/' + name_of_zip[:-4]) + + # try to unzip installer data an install workflow component + try: + with ZipFile(path_to_zip, 'r') as zObject: + # Extracting specific file in the zip into a specific location + zObject.extractall(path=path_to_temp) + zObject.close() + + if os.path.isdir(path_to_temp + '/' + name_of_zip[:-4]): + content_of_zip_directory = os.listdir(path_to_temp + '/' + name_of_zip[:-4]) + + # generate text file for RCE with install directory + file_for_install_path = open(path_to_tool_json_files + + 'absolutPathToUNICADOInstallDirectory.txt', 'w') + file_for_install_path.write(install_folder + '/') + file_for_install_path.close() + + # generate results and working directory + if not install_folder[-1] == '/': + install_folder = install_folder + '/' + if not os.path.isdir(install_folder + 'workflowResults'): + os.mkdir(install_folder + 'workflowResults') + if not os.path.isdir(install_folder + 'workingDirectoryRCE'): + os.mkdir(install_folder + 'workingDirectoryRCE') + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + file_for_tool_list.write('workflowResults' + '\n') + file_for_tool_list.write('workingDirectoryRCE' + '\n') + file_for_tool_list.close() + + # loop across all elements from temporary zip directory to install UNICADO + for content in content_of_zip_directory: + # open text file to write tool list + file_for_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + path_of_content = path_to_temp + '/' + name_of_zip[:-4] + '/' + content + if os.path.isdir(path_of_content): + if not content == 'softwareTools' and not content == 'workflowComponent': + # add tool name to tool list file + file_for_tool_list.write(content + '\n') + file_for_tool_list.close() + # copy design tool to install directory + shutil.copytree(path_of_content, install_folder + content) + + # check if current content equal to the unicado library directory + # -> if true: -> add path to local environment variables + if content == 'unicadoRuntimeLibs': + path_to_add_to_environment = install_folder + content + status_path_adding = write_path_to_environment(path_to_add_to_environment) + + # copy design tool to origin design tool directory + if not content == 'projects': + shutil.copytree(path_of_content, + path_to_tool_json_files + 'originUnicadoDesignTools/' + content) + + # copy aircraft projects to origin projects directory + else: + shutil.copytree(path_of_content, + path_to_tool_json_files + 'originUnicadoProjects/' + content) + + # copy tool json file to hidden .rce directory + if not content == 'projects' and not content == 'unicadoRuntimeLibs': + path_json_file = path_to_temp + '/' + name_of_zip[:-4] \ + + '/workflowComponent/jsonFiles/' + content + # check if a sjson file of current tool exist + if os.path.isdir(path_json_file): + shutil.copytree(path_json_file, path_to_tool_json_files + '/' + content) + # write paths to json configuration file of each module + with open(path_to_tool_json_files + content + '/configuration.json', + 'r+') as jsonFile: + json_data = json.load(jsonFile) + launch_settings = json_data['launchSettings'] + launch_settings[0]['rootWorkingDirectory'] =\ + install_folder + 'workingDirectoryRCE' + launch_settings[0]['toolDirectory'] = install_folder + content + json.dump(json_data, open(path_to_tool_json_files + content + + '/configuration.json', 'w'), + indent=2, sort_keys=False) + + elif content == 'softwareTools': + file_for_tool_list.close() + content_of_software_tools = os.listdir(path_of_content) + # loop across all tools in the directory to install os specific software version + for software_tool in content_of_software_tools: + file_for_tool_list = open(path_to_tool_json_files + + 'toolListUNICADOworkflow.txt', 'a') + # check if the current software tool contains the string 'gnuplot' or 'inkscape' + # if true: -> install os dependent software version + if 'gnuplot' in software_tool or 'inkscape' in software_tool: + if os.name == 'nt': + if not os.path.isdir(install_folder + 'gnuplot'): + shutil.copytree(path_of_content + '/gnuplot', + install_folder + 'gnuplot') + # copy tool to software design tool directory + shutil.copytree(path_of_content, + path_to_tool_json_files + + 'originUnicadoSoftwareTools/gnuplot') + # add tool name to tool list file + file_for_tool_list.write('gnuplot' + '\n') + + if not os.path.isdir(install_folder + 'inkscape'): + shutil.copytree(path_of_content + '/inkscape', + install_folder + 'inkscape') + # copy tool to software design tool directory + shutil.copytree(path_of_content, + path_to_tool_json_files + + 'originUnicadoSoftwareTools/inkscape') + # add tool name to tool list file + file_for_tool_list.write('inkscape' + '\n') + + # check for linux OS + if os.name == 'posix': + if not os.path.isdir(install_folder + 'gnuplot-linux'): + shutil.copytree(path_of_content + '/gnuplot-linux', + install_folder + 'gnuplot-linux') + # copy tool to software design tool directory + shutil.copytree(path_of_content, + path_to_tool_json_files + + 'originUnicadoSoftwareTools/gnuplot-linux') + # add tool name to tool list file + file_for_tool_list.write('gnuplot-linux' + '\n') + + if not os.path.isdir(install_folder + 'inkscape-linux'): + shutil.copytree(path_of_content + '/inkscape-linux', + install_folder + 'inkscape-linux') + # copy tool to software design tool directory + shutil.copytree(path_of_content, + path_to_tool_json_files + + 'originUnicadoSoftwareTools/inkscape-linux') + # add tool name to tool list file + file_for_tool_list.write('inkscape-linux' + '\n') + + file_for_tool_list.close() + + else: + # copy software tool to install directory + shutil.copytree(path_of_content + '/' + software_tool, + install_folder + software_tool) + # copy tool to software design tool directory + shutil.copytree(path_of_content + '/' + software_tool, + path_to_tool_json_files + + 'originUnicadoSoftwareTools/' + software_tool) + + # add tool name to tool list file + file_for_tool_list.write(software_tool + '\n') + file_for_tool_list.close() + + # copy tool json file to hidden .rce directory + path_json_file = path_to_temp + '/' + name_of_zip[:-4] \ + + '/workflowComponent/jsonFiles/' + software_tool + # check if a sjson file of current tool exist + if os.path.isdir(path_json_file): + shutil.copytree(path_json_file, + path_to_tool_json_files + '/' + software_tool) + # write paths to json configuration file of each module + with open(path_to_tool_json_files + software_tool + + '/configuration.json', 'r+') as jsonFile: + json_data = json.load(jsonFile) + launch_settings = json_data['launchSettings'] + launch_settings[0]['rootWorkingDirectory'] = \ + install_folder + 'workingDirectoryRCE' + launch_settings[0]['toolDirectory'] = install_folder + software_tool + json.dump(json_data, open(path_to_tool_json_files + software_tool + + '/configuration.json', 'w'), + indent=2, sort_keys=False) + + # installation of standalone workflow components + elif content == 'workflowComponent': + if not os.path.isdir(path_of_content + '/UNICADOworkflow'): + os.makedirs(path_of_content + '/UNICADOworkflow') + + shutil.copytree(path_of_content + '/UNICADOworkflow', + install_folder + '/workingDirectoryRCE/UNICADOworkflow') + + # copy json files to origin design tool directory + if not os.path.isdir(path_to_tool_json_files + + 'originUnicadoDesignTools/UNICADOworkflow/jsonFiles'): + os.makedirs(path_to_tool_json_files + + 'originUnicadoDesignTools/UNICADOworkflow/jsonFiles') + + list_of_json_files = os.listdir(path_of_content + '/jsonFiles') + for json_file in list_of_json_files: + shutil.copytree(path_of_content + '/jsonFiles/' + json_file, + path_to_tool_json_files + + 'originUnicadoDesignTools/UNICADOworkflow/jsonFiles/' + + json_file) + + # check if the current operating system a linux os -> if true: -> Remove TiGL Viewer and change execution rights + if os.name == 'posix': + # call function to remove TiGL viewer workflow component and its connections within the workflow, since deprecated on linux + remove_tigl_entry_from_wf_file(install_folder + 'workingDirectoryRCE/UNICADOworkflow/UNICADOworkflow.wf') + # call function to change the execution rights on linux os of each module + change_execution_rights_on_linux(install_folder) + + # else condition: unzipping of installer data failed -> display error message and abort installation + else: + error_flag = True + + # delete temporary files from system after successfully installation + if os.path.isdir(path_to_temp + '/' + name_of_zip[:-4]): + shutil.rmtree(path_to_temp + '/' + name_of_zip[:-4]) + + except OSError: + error_flag = True + + # else condition: installer zip file not found -> display error message and abort installation + else: + error_flag = True + + # else condition: version file not found -> display error message and abort installation + else: + error_flag = True + + if not error_flag: + # check if the current operating system is windows -> if true: -> check if the installation directory is empty + # -> if true: -> delete install directory + if os.name == 'nt': + if os.path.isdir('C:/Programs/UNICADOworkflow'): + if not os.listdir('C:/Programs/UNICADOworkflow'): + os.rmdir('C:/Programs/UNICADOworkflow') + + # check if the parent directory of install directory is empty -> if true: -> delete parent directory + if not os.listdir('C:/Programs'): + os.rmdir('C:/Programs') + + # check if the current operating system is linux -> if true: -> check if the installation directory is empty + # -> if true: -> delete install directory + if os.name == 'posix': + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + if os.path.isdir(user_path_string + 'UNICADOworkflow'): + if not os.listdir(user_path_string + 'UNICADOworkflow'): + os.rmdir(user_path_string + 'UNICADOworkflow') + + # write absolute paths to json configuration file of each module + path_to_settings_file = user_path_string + '.rce/default/internal/' + if not os.path.isdir(user_path_string + '.rce/default/internal/'): + os.makedirs(user_path_string + '.rce/default/internal/') + + if not os.path.isfile(user_path_string + '.rce/default/internal/settings.json'): + json_data = {'rce.workspace.recentLocations': '', + 'rce.workspace.lastLocation': '', + 'rce.workspace.dontAskAgain': 'false'} + # Write JSON file + with open(user_path_string + '.rce/default/internal/settings.json', 'w') as jsonFile: + json.dump(json_data, open(path_to_settings_file + 'settings.json', 'w')) + + if os.path.isfile(user_path_string + '.rce/default/internal/settings.json'): + with open(path_to_settings_file + 'settings.json', 'r+') as jsonFile: + # check if settings.json file is empty + try: + json_data = json.load(jsonFile) + except OSError: + json_data = {'rce.workspace.recentLocations': '', + 'rce.workspace.lastLocation': '', + 'rce.workspace.dontAskAgain': 'false'} + workspace_location_rce = json_data['rce.workspace.recentLocations'] + + # check if the current operating system is windows + # -> if true: -> set windows specific path to workingDirectoryRCE as default path to RCE + if os.name == 'nt': + install_path = install_folder.replace('/', '\\') + alternate_path = install_path.replace('\\', '\\\\') + json_data['rce.workspace.lastLocation'] = install_path + 'workingDirectoryRCE' + if not install_path[0] + '\:' + '\\' + install_path[3:] + 'workingDirectoryRCE' in workspace_location_rce: + if not install_path[0] + '\:' + alternate_path[2:] + 'workingDirectoryRCE' \ + in workspace_location_rce: + json_data['rce.workspace.recentLocations'] = install_path[0] + '\:' + alternate_path[2:] \ + + 'workingDirectoryRCE' + ':' \ + + workspace_location_rce + + else: + str_start = workspace_location_rce.index( + install_path[0] + '\:' + alternate_path[2:] + 'workingDirectoryRCE') + str_end = workspace_location_rce.index('workingDirectoryRCE') + if str_start > 0: + sub_string_front = workspace_location_rce[0:str_start - 1] + total_length = len(workspace_location_rce) + new_string = workspace_location_rce[str_start:str_end + 19] + ':' + sub_string_front + if str_end + 19 < total_length: + sub_string_end = workspace_location_rce[str_end + 20:total_length] + new_string = new_string + ':' + sub_string_end + json_data['rce.workspace.recentLocations'] = new_string + + # check if the current operating system is linux + # -> if true: -> set linux specific path to workingDirectoryRCE as default path to RCE + if os.name == 'posix': + json_data['rce.workspace.lastLocation'] = install_folder + 'workingDirectoryRCE' + if not install_folder + 'workingDirectoryRCE' in workspace_location_rce: + json_data['rce.workspace.recentLocations'] = install_folder + 'workingDirectoryRCE' + ':' \ + + workspace_location_rce + + # check if rce.workspace.dontAskAgain exist and set to false + if 'rce.workspace.dontAskAgain' in json_data: + json_data['rce.workspace.dontAskAgain'] = 'false' + else: + json_data['rce.workspace.dontAskAgain'] = 'false' + + json.dump(json_data, open(path_to_settings_file + 'settings.json', 'w')) + + else: + if os.path.isfile(path_to_tool_json_files + 'standAloneInstallationFlag.dat'): + os.remove(path_to_tool_json_files + 'standAloneInstallationFlag.dat') \ No newline at end of file diff --git a/installer/sub_functions/integration_step.py b/installer/sub_functions/integration_step.py new file mode 100644 index 0000000000000000000000000000000000000000..4ae865a49856ad0c2a4454e6bf707b72a83d87fe --- /dev/null +++ b/installer/sub_functions/integration_step.py @@ -0,0 +1,196 @@ +# imports for python +import os +import time +import shutil +from PyQt5.QtCore import QCoreApplication +from sub_functions.check_tool import check_tool +from sub_functions.write_json_file import write_json_file +from sub_functions.repository_integration import repository_integration + + +# function to handle module integration steps +def integration_step(self): + # handle ui objects for visibility + self.welcome_panel.setVisible(False) + self.header_panel.setVisible(True) + self.integration_panel.setVisible(True) + self.update_button.setVisible(False) + self.update_button.setEnabled(False) + self.uninstall_button.setVisible(False) + self.uninstall_button.setEnabled(False) + self.next_button.setVisible(True) + self.next_button.setEnabled(False) + self.next_button.setText('Next >') + self.cancel_button.setText('Cancel') + self.back_button.setVisible(True) + self.back_button.setEnabled(True) + self.integration_button.setVisible(False) + self.integration_button.setEnabled(False) + self.tool_name_panel.setVisible(True) + + # set description to text fields + self.header_text_label_1.setText('Welcome to the UNICADO module integration tool.') + self.header_text_label_2.setText('This tool will guide you through the model integration.') + self.integration_text_label_1.setText('Please enter the name of tool to be integrated and click OK.\n' + "Attention: The tool name must be written in 'camelCase'" + " and consist of two words.\n" + 'As example: "initialSizing".') + + # check if the module integration tool is running the first time in the current UNICADOworkflow version + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + path_to_tool_json_files = user_path_string + '.rce/default/integration/tools/common/' + if not os.path.isfile(path_to_tool_json_files + 'standardToolListUNICADOworkflow.txt'): + shutil.copyfile(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', + path_to_tool_json_files + 'standardToolListUNICADOworkflow.txt') + + tool_name = self.tool_name_panel_edit.text() + if not tool_name == 'Please enter name of tool to be integrated': + check_tool(self) + + string = self.integration_text_label_0.text() + hex_string = '' + for char in string: + dual = ' '.join(format(ord(x), 'b') for x in char) + hex_string += f'{int(dual, 2):X}' + + self.integration_text_label_0.setText(hex_string) + + # update ui + QCoreApplication.processEvents() + + +# function to perform tool integration +def perform_integration(self): + # initialize local parameter + repo_error = 0 + tool_flag = False + error_flag = False + install_path = str() + installed_tool_list = [] + tool_name = self.tool_name_panel_edit.text() + group_name = self.group_name_panel_edit.text() + + # get install path of UNICADOworkflow + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + path_to_tool_json_files = user_path_string + '.rce/default/integration/tools/common/' + + # check if the file containing the installation path exists + if os.path.isfile(path_to_tool_json_files + 'absolutPathToUNICADOInstallDirectory.txt'): + read_install_path = open(path_to_tool_json_files + 'absolutPathToUNICADOInstallDirectory.txt', 'r') + install_path = read_install_path.read() + else: + error_flag = True + + # check if the file containing the names of installed tools exists + if os.path.isfile(path_to_tool_json_files + 'toolListUNICADOworkflow.txt'): + installed_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'r') + for tool in installed_tool_list: + if tool_name == tool[:-1]: + tool_flag = True + installed_tool_list.close() + installed_tool_list = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + else: + error_flag = True + + # start integration if no error is occurred before + if not error_flag: + # call function to write user data from ui to configuration.json file of new tool + error_flag = write_json_file(self, path_to_tool_json_files, install_path, tool_name, group_name, error_flag, + False, '') + + if not error_flag: + # copy tool to be integrated from locale directory to unicado installation directory + tool_path = self.tool_path_panel_edit.text() + # check if the tool overwriting option selected + # -> if true: -> try to delete selected tool from install directory + if self.integration_radio_button_overwrite.isChecked(): + if os.path.isdir(install_path + tool_name): + shutil.rmtree(install_path + tool_name) + + # check if the entered tool path not inside the unicado installation + # -> if true: -> copy tool to installation directory + if not os.path.isdir(install_path + tool_name): + shutil.copytree(tool_path, install_path + tool_name) + + # write tool name to list of installed tools text file + if tool_flag: + installed_tool_list.close() + else: + installed_tool_list.write(tool_name + '\n') + installed_tool_list.close() + + # check if the repository integration is selected + if self.integration_checkbox.isChecked(): + # call function to integrate or overwrite tool in working branch of repository + repo_error, error_flag = repository_integration(self) + print(repo_error) + print(error_flag) + + # an error is occurred -> prepare ui to display error message + if error_flag: + installed_tool_list.close() + self.welcome_panel.setVisible(True) + self.install_and_finish_panel.setVisible(False) + self.header_panel.setVisible(False) + self.update_button.setVisible(False) + self.update_button.setEnabled(True) + self.cancel_button.setText('Abort') + self.cancel_button.setEnabled(True) + self.cancel_button.setVisible(True) + self.next_button.setVisible(False) + QCoreApplication.processEvents() + if repo_error == 0: + self.welcome_text_label_1.setText('Attention! An Error has occurred!') + self.install_text_label_1.setText('Tool can not be integrated!') + self.install_text_label_2.setText('Please reinstall UNICADOworkflow. ' + 'After that please start the tool integration again.') + QCoreApplication.processEvents() + + if repo_error == 1: + self.welcome_text_label_1.setText('Attention! An Error has occurred!') + self.install_text_label_1.setText('Tool could be integrated, but path to the aircraft design repository ' + 'could not be found. Repository integration failed!') + self.install_text_label_2.setText('Please check your repository installation and add the tool to the ' + 'repository manually.') + self.continue_text_label.setText('To quit the installer, click Finish.') + self.cancel_button.setText('Finish') + QCoreApplication.processEvents() + + if repo_error == 2: + self.welcome_text_label_1.setText('Attention! An Error has occurred!') + self.install_text_label_1.setText('Tool could be integrated, but the automatic git checkout could not be ' + 'performed. Repository integration failed!') + self.install_text_label_2.setText('Please check your repository installation and add the tool to the ' + 'repository manually.') + self.continue_text_label.setText('To quit the installer, click Finish.') + self.cancel_button.setText('Finish') + QCoreApplication.processEvents() + + if repo_error == 3: + self.welcome_text_label_1.setText('Attention! An Error has occurred!') + self.install_text_label_1.setText('Tool could be integrated, but the automatic json file generation failed.' + ' Repository integration only partially done!') + self.install_text_label_2.setText('Please check your repository installation and add the missing files to ' + 'the repository manually.') + self.continue_text_label.setText('To quit the installer, click Finish.') + self.cancel_button.setText('Finish') + QCoreApplication.processEvents() + + if repo_error == 4: + self.welcome_text_label_1.setText('Attention! An Error has occurred!') + self.install_text_label_1.setText('Tool could be integrated, but the automatic repository commit failed. ' + 'Repository integration only partially done!') + self.install_text_label_2.setText('Please check your repository installation and perform the repository ' + 'commit manually.') + self.continue_text_label.setText('To quit the installer, click Finish.') + self.cancel_button.setText('Finish') + QCoreApplication.processEvents() + + else: + time.sleep(3) + + return error_flag diff --git a/installer/sub_functions/last_step.py b/installer/sub_functions/last_step.py new file mode 100644 index 0000000000000000000000000000000000000000..3945a8ee36e0963db24ee91b21336c95626acbc4 --- /dev/null +++ b/installer/sub_functions/last_step.py @@ -0,0 +1,134 @@ +# import for python +import os +from PyQt5 import QtCore +from sub_functions.check_tool import check_tool + + +def last_step(self): + """ Call function to handle the back button. + + :rtype: object + """ + + header_panel_visibility = self.header_panel.isVisible() + install_path_panel_visibility = self.install_path_panel.isVisible() + integration_panel_visibility = self.integration_panel.isVisible() + repository_path_panel_visibility = self.repository_path_panel.isVisible() + + if header_panel_visibility: + header_string = self.header_text_label_1.text() + if header_string == 'UNICADO workflow will be installed': + self.header_text_label_1.setText('Select UNICADO Repository Location') + self.header_text_label_2.setText('Select the folder in which UNICADO repositories lies.') + + if header_string == 'Select UNICADO Repository Location': + self.header_text_label_1.setText('Choose Install Location') + self.header_text_label_2.setText('Chose the folder in which to install UNICADOworkflow.') + + if install_path_panel_visibility: + self.welcome_panel.setVisible(True) + self.install_path_panel.setVisible(False) + self.header_panel.setVisible(False) + self.back_button.setVisible(False) + + elif integration_panel_visibility: + tool_name_panel_visibility = self.tool_name_panel.isVisible() + tool_path_panel_visibility = self.tool_path_panel.isVisible() + header_string = self.header_text_label_1.text() + if tool_name_panel_visibility or self.next_button.text() == 'Uninstall' or self.next_button.text() == 'Update': + self.welcome_panel.setVisible(True) + self.integration_panel.setVisible(False) + self.header_panel.setVisible(False) + self.update_button.setVisible(True) + self.update_button.setEnabled(True) + self.uninstall_button.setVisible(True) + self.uninstall_button.setEnabled(True) + self.next_button.setVisible(False) + self.next_button.setEnabled(False) + self.back_button.setVisible(False) + self.back_button.setEnabled(False) + self.integration_button.setVisible(True) + self.integration_button.setEnabled(True) + + elif tool_path_panel_visibility or header_string == 'Final steps to delete an integrated tool': + self.next_button.setText('Next >') + self.next_button.setEnabled(True) + self.tool_path_panel.setVisible(False) + self.tool_name_panel.setVisible(True) + self.tool_name_panel_edit_button.setVisible(True) + self.integration_combo_box.setVisible(False) + self.integration_text_label_2.setVisible(True) + self.integration_text_label_2.setGeometry(QtCore.QRect(20, 170, 410, 80)) + self.integration_text_label_4.setGeometry(QtCore.QRect(20, 200, 410, 80)) + self.integration_text_label_4.setVisible(False) + self.header_text_label_1.setText('Welcome to the UNICADO module integration tool.') + self.header_text_label_2.setText('This tool will guide you through the model integration.') + self.integration_text_label_1.setText( + 'Please enter the name of tool to be integrated and click OK.\n' + "Attention: The tool name must be written in 'camelCase'" + " and consist of two words.\n" + 'As example: "initialSizing".') + self.integration_text_label_3.setVisible(False) + self.group_name_panel.setVisible(False) + if self.integration_radio_button_delete.isChecked() or self.integration_radio_button_overwrite.isChecked(): + self.button_name_panel.setVisible(True) + + else: + self.next_button.setEnabled(False) + self.next_button.setText('Next >') + self.header_text_label_1.setText('Please select tool path and workflow group') + self.header_text_label_2.setText('') + self.integration_text_label_2.setVisible(False) + self.integration_text_label_1.setText('Please enter the local system path to the integrating tool.') + self.tool_name_panel.setVisible(False) + self.tool_name_panel_edit_button.setVisible(False) + self.tool_name_panel.setVisible(False) + self.tool_path_panel.setVisible(True) + self.tool_path_panel_browse_button.setText('Browse') + self.button_group_panel.setVisible(False) + self.integration_text_label_5.setVisible(False) + self.checkbox_panel.setVisible(False) + + tool_name = self.tool_name_panel_edit.text() + local_tool_folder = self.tool_path_panel_edit.text() + if not local_tool_folder == 'Click Browse to enter local tool path': + content_of_directory = os.listdir(local_tool_folder) + if os.path.isdir(local_tool_folder + '/' + tool_name): + self.integration_text_label_3.setVisible(True) + self.integration_combo_box.setVisible(False) + self.integration_text_label_3.setText( + 'Attention: There is no executable file with the entered tool name in the selected directory. ' + 'Please change path.') + else: + if tool_name in content_of_directory or tool_name + '.exe' in content_of_directory: + self.integration_text_label_3.setVisible(True) + self.integration_combo_box.setVisible(True) + self.integration_text_label_3.setText( + 'Please select the group in which the tool is to be integrated.') + selected_group = self.integration_combo_box.currentText() + if selected_group == '- please select a tool group -' or selected_group == '- other -': + self.integration_text_label_4.setVisible(False) + self.next_button.setEnabled(False) + self.group_name_panel.setVisible(False) + if selected_group == '- other -': + self.group_name_panel.setVisible(True) + input_string = self.group_name_panel_edit.text() + if not input_string == 'Please enter tool group name.': + check_tool(self) + else: + self.group_name_panel.setVisible(False) + self.integration_text_label_4.setText( + "Please click 'Next' to continue the tool integration.") + self.integration_text_label_4.setVisible(True) + self.next_button.setEnabled(True) + else: + self.integration_text_label_3.setVisible(True) + self.integration_text_label_3.setText( + 'Attention: There is no executable file with the entered tool name ' + 'in the selected directory. ' + 'Please change path.') + + elif repository_path_panel_visibility: + self.install_path_panel.setVisible(True) + self.repository_path_panel.setVisible(False) + self.next_button.setEnabled(True) diff --git a/installer/sub_functions/line_edit.py b/installer/sub_functions/line_edit.py new file mode 100644 index 0000000000000000000000000000000000000000..e3abbdd1bba536fc46fcbcc05f6f314877b18383 --- /dev/null +++ b/installer/sub_functions/line_edit.py @@ -0,0 +1,108 @@ +def line_edit(self): + """ Call function to handle the line edit field. + + :rtype: object + """ + + ''' imports for python ''' + import os + import shutil + import configparser + from PyQt5.QtCore import QCoreApplication + + # check if the installation panel is shown in the installer application + # -> if true: -> read install path from line edit-field of installer application + install_path_panel_visibility = self.install_path_panel.isVisible() + repository_path_panel_visibility = self.repository_path_panel.isVisible() + checkbox_panel_visibility = self.checkbox_panel.isVisible() + install_folder = self.install_path_line_edit.text() + install_folder = str(install_folder.replace(os.sep, '/')) + if install_path_panel_visibility: + # check if the current operating system is windows -> if true: -> check for necessary administrator rights + if os.name == 'nt': + # check if the 'C:/Program Files' is not existing -> if true: + # -> try to generate a test folder inside of current hard drive directory to check for necessary administrator rights + try: + if os.path.isdir(install_folder): + os.mkdir(install_folder + '/test') + shutil.rmtree(install_folder + '/test') + self.install_path_text_label_1.setText("ATTENTION: If UNICADOworkflow is to be installed in " + "C:/Program Files or C:/Program Files (x86), the installer and " + "RCE must be executed with administrator rights. Ensure that you" + " have these rights.") + QCoreApplication.processEvents() + + # exception handling if necessary administrator rights not existing + # -> set UNICADO install path to 'C:/Programs/UNICADOworkflow/' + except OSError: + install_folder = 'C:/Programs/UNICADOworkflow/' + self.install_path_text_label_1.setText("ATTENTION: Please note that you do not have the necessary " + "administrator rights for the installation in the selected " + "directory. Installation continues in " + "C:/Programs/UNICADOworkflow/.") + self.install_path_line_edit.setText(install_folder) + QCoreApplication.processEvents() + + # check if the select repository panel is shown in the installer application + # -> if true: -> check if all necessary repositories exist and set git url path + if repository_path_panel_visibility: + self.next_button.setEnabled(False) + QCoreApplication.processEvents() + input_string = self.repository_path_line_edit.text() + input_string = str(input_string.replace(os.sep, '/')) + if os.path.isdir(input_string): + folders_in_directory = os.listdir(input_string) + for folder in folders_in_directory: + path_to_check = input_string + '/' + folder + '/.git' + if os.path.isdir(path_to_check): + if os.path.isfile(input_string + folder + '/.git/config'): + config = configparser.ConfigParser() + config.read(input_string + folder + '/.git/config') + if config.has_section('remote "origin"'): + url = config['remote "origin"']['url'] + if url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rADDITIONALSOFTWARE.git': + self.next_button.setEnabled(True) + QCoreApplication.processEvents() + + if checkbox_panel_visibility: + stars = str() + string = self.integration_text_label_0.text() + user_input = self.checkbox_panel_text_label_0.text() + input_string = self.checkbox_panel_edit.text().upper() + + # password manager to handle user input string + if input_string[:-1] == 'ENTER PASSWORD': + input_string = '' + self.integration_checkbox_show.setVisible(False) + + if len(input_string) > 0: + self.integration_checkbox_show.setVisible(True) + diff = len(input_string) - len(user_input) + if diff < 0: + if diff == -1: + self.checkbox_panel_text_label_0.setText(user_input[:-1]) + elif -1*diff >= len(input_string): + self.checkbox_panel_text_label_0.setText(input_string) + else: + self.checkbox_panel_text_label_0.setText(user_input[:diff]) + elif diff == 0: + self.checkbox_panel_text_label_0.setText(input_string) + else: + self.checkbox_panel_text_label_0.setText(user_input + input_string[len(user_input):len(user_input)+diff]) + + if string == self.checkbox_panel_text_label_0.text() and\ + (self.integration_radio_button_yes.isChecked() or self.integration_radio_button_no.isChecked()): + self.next_button.setEnabled(True) + + if not self.integration_checkbox_show.isChecked(): + for _ in input_string: + stars += '*' + else: + stars = input_string + else: + stars = 'enter password' + self.checkbox_panel_text_label_0.setText('') + self.integration_checkbox_show.setVisible(False) + + self.checkbox_panel_edit.setText(stars) + QCoreApplication.processEvents() diff --git a/installer/sub_functions/next_step.py b/installer/sub_functions/next_step.py new file mode 100644 index 0000000000000000000000000000000000000000..edfe8dd17f52842531a9fe72ed5a3b9564d5f703 --- /dev/null +++ b/installer/sub_functions/next_step.py @@ -0,0 +1,307 @@ +# imports for python +import os +import shutil +from PyQt5 import QtCore +from PyQt5.QtCore import QCoreApplication +from sub_functions.check_tool import check_tool +from sub_functions.delete_tool import delete_tool +from sub_functions.update_steps import update_unicado +from sub_functions.uninstall_steps import uninstall_unicado +from sub_functions.integration_step import perform_integration + + +def next_step(self): + """ Call function to handle the next button. + + :rtype: object + """ + + welcome_panel_visibility = self.welcome_panel.isVisible() + header_panel_visibility = self.header_panel.isVisible() + install_path_panel_visibility = self.install_path_panel.isVisible() + repository_path_panel_visibility = self.repository_path_panel.isVisible() + integration_panel_visibility = self.integration_panel.isVisible() + + if header_panel_visibility: + header_string = self.header_text_label_1.text() + if header_string == 'Choose Install Location': + self.header_text_label_1.setText('Select UNICADO Repository Location') + self.header_text_label_2.setText('Select the folder in which UNICADO repositories lies.') + + if self.install_radio_button_repo.isChecked(): + if header_string == 'Select UNICADO Repository Location': + self.header_text_label_1.setText('UNICADO workflow will be installed') + self.header_text_label_2.setText('This may take a moment.') + + if welcome_panel_visibility: + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + check_for_existing_installation = os.path.isfile(user_path_string + '.rce/default/integration/tools/common/' + 'absolutPathToUNICADOInstallDirectory.txt') + check_for_existing_repository_file = os.path.isfile(user_path_string + '.rce/default/integration/tools/common/' + 'absolutPathToUNICADOrepositoryDirectory' + '.txt') + check_for_existing_tool_file = os.path.isfile(user_path_string + '.rce/default/integration/tools/common/' + 'toolListUNICADOworkflow.txt') + + if check_for_existing_installation or check_for_existing_repository_file or check_for_existing_tool_file: + self.next_button.setVisible(False) + self.integration_button.setVisible(True) + self.uninstall_button.setVisible(True) + self.update_button.setVisible(True) + if self.install_radio_button_repo.isChecked(): + self.update_button.setEnabled(True) + self.welcome_text_label_1.setText('UNICADOworkflow is already installed') + self.install_text_label_1.setText('Please select if you want to Uninstall the existing installation or ' + 'Update to the current version.') + self.install_text_label_2.setText('If you want to add a module click Tool Integration.') + self.continue_text_label.setText('To abort the installer, click Cancel.') + else: + self.update_button.setEnabled(False) + self.welcome_text_label_1.setText('UNICADOworkflow is already installed') + self.install_text_label_1.setText('Please select if you want to Uninstall the existing installation or ' + 'if you want to Integrate a new tool.') + self.install_text_label_2.setText('Attention: The Update function is not available for standalone ' + 'installations.') + self.continue_text_label.setText('To abort the installer, click Cancel.') + else: + if os.name == 'posix': + self.install_path_line_edit.setText(user_path_string + 'UNICADOworkflow') + self.header_panel.setVisible(True) + self.install_path_panel.setVisible(True) + self.back_button.setVisible(True) + self.welcome_panel.setVisible(False) + total, used, free = shutil.disk_usage("/") + self.install_path_text_label_3.setText("Disk space available: %d GB" % (used // (2 ** 30))) + + elif install_path_panel_visibility: + if self.install_radio_button_repo.isChecked(): + self.repository_path_panel.setVisible(True) + self.install_path_panel.setVisible(False) + self.next_button.setEnabled(False) + else: + self.header_text_label_1.setText('UNICADO workflow will be installed') + self.header_text_label_2.setText('This may take a moment.') + self.install_and_finish_panel.setVisible(True) + self.repository_path_panel.setVisible(False) + self.back_button.setVisible(False) + self.next_button.setText('Install') + self.next_button.released.connect(self.installUNICADO) + + elif repository_path_panel_visibility: + self.install_and_finish_panel.setVisible(True) + self.repository_path_panel.setVisible(False) + self.back_button.setVisible(False) + self.next_button.setText('Install') + self.next_button.released.connect(self.installUNICADO) + + elif integration_panel_visibility: + tool_name_panel_visibility = self.tool_name_panel.isVisible() + tool_path_panel_visibility = self.tool_path_panel.isVisible() + radio_button_panel_visibility = self.button_group_panel.isVisible() + + if self.next_button.text() == 'Uninstall': + uninstall_unicado(self) + + if self.next_button.text() == 'Update': + update_unicado(self) + + if self.next_button.text() == 'Delete': + self.integration_text_label_1.setText('Please wait until the integrated tool is deleted.') + self.cancel_button.setText('Finish') + self.cancel_button.setEnabled(False) + self.next_button.setVisible(False) + self.back_button.setVisible(False) + QCoreApplication.processEvents() + + # call function to delete an integrated non-basic tool + error_flag = delete_tool(self) + + if error_flag: + self.welcome_panel.setVisible(True) + self.integration_panel.setVisible(False) + self.header_panel.setVisible(False) + self.welcome_text_label_1.setText('Attention! An internal error has occurred!') + self.install_text_label_1.setText('Selected tool can not be deleted!') + self.install_text_label_2.setText('Please reinstall UNICADOworkflow or contact the developer team.') + self.update_button.setVisible(False) + self.update_button.setEnabled(True) + self.cancel_button.setText('Abort') + self.cancel_button.setEnabled(True) + self.cancel_button.setVisible(True) + self.next_button.setVisible(False) + QCoreApplication.processEvents() + + else: + self.header_text_label_1.setText('Deleting of tool finished') + self.cancel_button.setEnabled(True) + self.integration_button.setVisible(True) + self.integration_button.setEnabled(True) + self.integration_button_group.setExclusive(False) + self.integration_radio_button_yes.setChecked(False) + self.integration_radio_button_no.setChecked(False) + self.integration_button_group.setExclusive(True) + self.integration_button_group_name.setExclusive(False) + self.integration_radio_button_delete.setChecked(False) + self.integration_radio_button_overwrite.setChecked(False) + self.integration_button_group_name.setExclusive(True) + self.integration_text_label_0.setText("UNICADO") + self.integration_text_label_2.setGeometry(QtCore.QRect(20, 170, 410, 80)) + self.integration_text_label_4.setGeometry(QtCore.QRect(20, 200, 410, 80)) + self.tool_name_panel_edit.setText('Please enter name of tool to be integrated') + self.tool_name_panel_edit_button.setVisible(True) + self.tool_path_panel_edit.setText('Click Browse to enter local tool path') + self.integration_combo_box.clear() + self.integration_combo_box.addItems(['- please select a tool group -', 'postProcessing', 'preSizing', + 'sizingLoop', 'visualization', '- other -']) + self.group_name_panel_edit.setText("Please enter tool group name.") + self.integration_checkbox.setChecked(False) + QCoreApplication.processEvents() + + return + + if tool_name_panel_visibility: + if not self.integration_radio_button_delete.isChecked(): + self.next_button.setEnabled(False) + self.button_name_panel.setVisible(False) + self.header_text_label_1.setText('Please select tool path and workflow group') + self.header_text_label_2.setText('') + self.integration_text_label_2.setVisible(False) + self.integration_text_label_2.setGeometry(QtCore.QRect(20, 200, 410, 80)) + self.integration_text_label_4.setGeometry(QtCore.QRect(20, 230, 410, 80)) + self.integration_text_label_1.setText('Please enter the local system path to the integrating tool.') + self.tool_name_panel.setVisible(False) + self.tool_name_panel_edit_button.setVisible(False) + self.tool_name_panel.setVisible(False) + self.tool_path_panel.setVisible(True) + self.tool_path_panel_browse_button.setText('Browse') + + tool_name = self.tool_name_panel_edit.text() + local_tool_folder = self.tool_path_panel_edit.text() + + if not local_tool_folder == 'Click Browse to enter local tool path': + content_of_directory = os.listdir(local_tool_folder) + if os.path.isdir(local_tool_folder + '/' + tool_name): + self.integration_text_label_3.setVisible(True) + self.integration_combo_box.setVisible(False) + self.integration_text_label_3.setText( + 'Attention: There is no executable file with the entered tool name ' + 'in the selected directory. Please change path.') + else: + if tool_name in content_of_directory or tool_name + '.exe' in content_of_directory: + self.integration_text_label_3.setVisible(True) + self.integration_combo_box.setVisible(True) + self.integration_text_label_3.setText('Please select the group in which the tool is to be' + ' integrated.') + selected_group = self.integration_combo_box.currentText() + if selected_group == '- please select a tool group -' or selected_group == '- other -': + self.integration_text_label_4.setVisible(False) + self.next_button.setEnabled(False) + self.group_name_panel.setVisible(False) + if selected_group == '- other -': + self.group_name_panel.setVisible(True) + input_string = self.group_name_panel_edit.text() + if not input_string == 'Please enter tool group name.': + check_tool(self) + else: + self.group_name_panel.setVisible(False) + self.integration_text_label_4.setText( + "Please click 'Next' to continue the tool integration.") + self.integration_text_label_4.setVisible(True) + self.next_button.setEnabled(True) + else: + self.integration_text_label_3.setVisible(True) + self.integration_text_label_3.setText( + 'Attention: There is no executable file with the entered tool name in the ' + 'selected directory. Please change path.') + else: + self.next_button.setText('Delete') + self.integration_text_label_1.setText('Attention: You have selected delete tool.\n' + 'If you really want to delete the selected tool, click "Delete" ' + 'or click "Back" to return to the previous page.') + self.header_text_label_1.setText('Final steps to delete an integrated tool') + self.header_text_label_2.setText('') + self.integration_text_label_2.setVisible(False) + self.integration_text_label_3.setVisible(False) + self.integration_text_label_4.setVisible(False) + self.tool_name_panel.setVisible(False) + self.button_name_panel.setVisible(False) + + if tool_path_panel_visibility: + if self.integration_radio_button_overwrite.isChecked(): + self.next_button.setText('Overwrite') + self.integration_checkbox.setText('Repository overwrite') + self.header_text_label_1.setText('Final steps for overwrite the selected tool') + self.integration_text_label_1.setText('Please select if the tool to be overwrite should abort the ' + 'workflow loop if it returns an error code.') + self.integration_text_label_5.setText( + 'Please select if the tool should also be overwritten in your working branch of the aircraft ' + 'design repository.\n' + 'Attention: The developer password is required.') + self.integration_text_label_5.setVisible(True) + else: + self.next_button.setText('Integrate') + self.integration_checkbox.setText('Repository integration') + self.header_text_label_1.setText('Final steps for the tool integration') + self.integration_text_label_1.setText('Please select if the tool to be integrated should abort the ' + 'workflow loop if it returns an error code.') + self.integration_text_label_5.setText( + 'Please activate the following checkbox if the tool should also be integrated into your ' + 'aircraft design repository.\n' + 'Attention: The developer password is required.') + self.integration_text_label_5.setVisible(False) + + self.next_button.setEnabled(False) + self.header_text_label_2.setText('') + self.integration_text_label_3.setVisible(False) + self.integration_text_label_4.setVisible(False) + self.tool_path_panel.setVisible(False) + self.group_name_panel.setVisible(False) + self.integration_combo_box.setVisible(False) + self.button_group_panel.setVisible(True) + self.checkbox_panel.setVisible(True) + + if radio_button_panel_visibility: + self.header_text_label_1.setText('Integration in progress') + self.integration_text_label_1.setText('Please wait until the tool integration is completed.') + self.button_group_panel.setVisible(False) + self.checkbox_panel.setVisible(False) + self.back_button.setEnabled(False) + self.back_button.setVisible(False) + self.next_button.setEnabled(False) + self.cancel_button.setText('Finish') + self.cancel_button.setEnabled(False) + self.integration_text_label_5.setVisible(False) + QCoreApplication.processEvents() + + # function call to perform tool integration + error_flag = perform_integration(self) + + # reset ui settings to allow next tool integration + if not error_flag: + self.header_text_label_1.setText('Integration finished') + self.next_button.setVisible(False) + self.cancel_button.setEnabled(True) + self.integration_button.setVisible(True) + self.integration_button.setEnabled(True) + self.integration_button_group.setExclusive(False) + self.integration_radio_button_yes.setChecked(False) + self.integration_radio_button_no.setChecked(False) + self.integration_button_group.setExclusive(True) + self.integration_button_group_name.setExclusive(False) + self.integration_radio_button_delete.setChecked(False) + self.integration_radio_button_overwrite.setChecked(False) + self.integration_button_group_name.setExclusive(True) + self.integration_text_label_0.setText("UNICADO") + self.integration_text_label_2.setGeometry(QtCore.QRect(20, 170, 410, 80)) + self.integration_text_label_4.setGeometry(QtCore.QRect(20, 200, 410, 80)) + self.tool_name_panel_edit.setText('Please enter name of tool to be integrated') + self.tool_name_panel_edit_button.setVisible(True) + self.tool_path_panel_edit.setText('Click Browse to enter local tool path') + self.integration_combo_box.clear() + self.integration_combo_box.addItems(['- please select a tool group -', 'postProcessing', 'preSizing', + 'sizingLoop', 'visualization', '- other -']) + self.group_name_panel_edit.setText("Please enter tool group name.") + self.integration_checkbox.setChecked(False) + QCoreApplication.processEvents() diff --git a/installer/sub_functions/remove_tigl_entry.py b/installer/sub_functions/remove_tigl_entry.py new file mode 100644 index 0000000000000000000000000000000000000000..ce511825fa8f36387e945f96d112ef8fbc621001 --- /dev/null +++ b/installer/sub_functions/remove_tigl_entry.py @@ -0,0 +1,83 @@ +"""Module removing the TiGL viewer application and its connectors from an RCE .wf file""" + +import json + + +def find_sub_dict_by_key_value(data, key, value) -> list | dict | None : + """Search for the sub-list in .wf file which + contains the tigl entry or the tigl identifier for the connector + + Depending on the structure of the json format sometimes the sub-dictionary is of type dict + and sometimes it is of type list, which is handled by this function. + + :param dict/list data: Here the .wf file is passed (in json format). + :param str key: A key word to be searched for within a dictionary or a list. + :param str value: The value of the respective key. + :returns: sub_result: Dictionary or list from within data, which match the key-value pair + + """ + if isinstance(data, dict): + if key in data and data[key] == value: + return data + for _, v in data.items(): + sub_result = find_sub_dict_by_key_value(v, key, value) + if sub_result: + return sub_result + elif isinstance(data, list): + for item in data: + sub_result = find_sub_dict_by_key_value(item, key, value) + if sub_result: + return sub_result + return None + + +def remove_sublist(data, sublist, key, value): + """ Removes a sub-list or a sub-dictionary from a dictionary or a list. + + If function find_sub_dict_by_key_value (above) was successful, + then there are two sublist: One with the tigl module entry and one with the + connector identifier. Both lists are removed from the json file + + :param dict/list data: Data could be a dictionary or a list + :param dict/list sublist: sub-list or sub-dict containing a key-value pair, which is removed. + :param str key: A key word to be searched for within a dictionary or a list. + :param str value: The value of the respective key. + :returns: - + """ + if isinstance(data, dict): + for _, v in data.items(): + remove_sublist(v, sublist, key, value) + if isinstance(data, list): + for item in data: + first_set = set(map(tuple, item)) + second_set = set(map(tuple, sublist)) + if second_set.symmetric_difference(first_set) == set() and item[key] == value: + data.remove(item) + + +def remove_tigl_entry_from_wf_file(wf_file_path): + """ Function which removes the TiGL viewer entry from the workflow file + + Function reads the workflow file in a specific json format. + It searches for the TiGL viewer entry and for the connections to the TiGL viewer. + Both elements are probably either existent in a dictionary or in a list containing + RCE specific information, which has to be removed. + + :params: str wf_file_path: .wf file path + :return: - + """ + with open(wf_file_path, 'r', encoding="utf-8") as input_file: + data = json.load(input_file) + + # Find the sub-dictionary containing the key-value pair "name": "TiGL Viewer" + result = find_sub_dict_by_key_value(data, "name", "TiGL Viewer") + if result: + # Save identifier of Tigl Viewer to delete the connector later + tigl_identifier = result["identifier"] + remove_sublist(data, result, "name", "TiGL Viewer") # Remove Tigl viewer sub dictionary + # Find connector of Tigl viewer + result = find_sub_dict_by_key_value(data, "target", tigl_identifier) + if result: + remove_sublist(data, result, "target", tigl_identifier) # Remove Tigl viewer connector + with open(wf_file_path, 'w', encoding="utf-8") as output_file: + json.dump(data, output_file, indent=2, separators=(', ',' : ')) diff --git a/installer/sub_functions/repository_integration.py b/installer/sub_functions/repository_integration.py new file mode 100644 index 0000000000000000000000000000000000000000..22c266988884d7c1d302a1d8afc6e544525cd540 --- /dev/null +++ b/installer/sub_functions/repository_integration.py @@ -0,0 +1,130 @@ +import os +import shutil + +#import git +import configparser +from sub_functions.write_json_file import write_json_file + + +def repository_integration(self): + # initialize local parameter + repo_error = 0 + error_flag = False + path_to_aircraft_design_tools = str() + tool_name = self.tool_name_panel_edit.text() + group_name = self.group_name_panel_edit.text() + tool_directory = self.tool_path_panel_edit.text() + + # try to read repository path file + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + path_to_repository_file = user_path_string + '.rce/default/integration/tools/common/' \ + 'absolutPathToUNICADOrepositoryDirectory.txt' + + if os.path.isfile(path_to_repository_file): + read_repository_path = open(path_to_repository_file, 'r') + repository_path = read_repository_path.read() + '' + read_repository_path.close() + else: + repo_error = 1 + return repo_error, error_flag + + # try to read repository url from repository configuration file + try: + files = os.listdir(repository_path) + for fileName in files: + check_if_file_is_directory = os.path.isdir(repository_path + fileName) + if check_if_file_is_directory: + check_git_folder = os.path.isdir(repository_path + fileName + '/.git') + if check_git_folder: + check_config_file = os.path.isfile(repository_path + fileName + '/.git/config') + if check_config_file: + config = configparser.ConfigParser() + config.read(repository_path + fileName + '/.git/config') + if config.has_section('remote "origin"'): + ssh_url = config['remote "origin"']['url'] + if ssh_url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rAircraftDesign.git': + path_to_aircraft_design_tools = repository_path + fileName + '/' + print(path_to_aircraft_design_tools) + repo_error = 0 + + except OSError: + error_flag = True + repo_error = 1 + return repo_error, error_flag + + if repo_error == 0: + path_to_tool_json_files = path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles/' + # try: + # # checkout develop branch from aircraft design repository + # local_branches = os.listdir(path_to_aircraft_design_tools + '.git/refs/heads') + # repo = git.Repo(path_to_aircraft_design_tools) + # current_branch = repo.active_branch.name + # if tool_name in local_branches: + # repo.git.switch(tool_name, '-f') + # elif current_branch == 'develop': + # repo.git.checkout("develop", b=tool_name) + # else: + # repo.git.switch('develop', '-f') + # repo.git.checkout("develop", b=tool_name) + # + # except OSError: + # error_flag = True + # repo_error = 2 + # return repo_error, error_flag + # + # # copy tool to local repository + # list_of_tools_repository = os.listdir(path_to_aircraft_design_tools) + # if tool_name in list_of_tools_repository: + # list_of_tool_content = os.listdir(tool_directory) + # list_ot_repository_content = os.listdir(path_to_aircraft_design_tools + tool_name) + # for element in list_of_tool_content: + # if element in list_ot_repository_content: + # if os.path.isdir(tool_directory + '/' + element): + # shutil.rmtree(path_to_aircraft_design_tools + tool_name + '/' + element) + # shutil.copytree(tool_directory + '/' + element, path_to_aircraft_design_tools + tool_name + # + '/' + element) + # repo.git.add(path_to_aircraft_design_tools + tool_name + '/' + element) + # + # elif os.path.isfile(tool_directory + '/' + element): + # shutil.copyfile(tool_directory + '/' + element, path_to_aircraft_design_tools + tool_name + # + '/' + element) + # repo.git.add(path_to_aircraft_design_tools + tool_name + '/' + element) + # else: + # if os.path.isdir(tool_directory + '/' + element): + # shutil.copytree(tool_directory + '/' + element, path_to_aircraft_design_tools + tool_name + # + '/' + element) + # repo.git.add(path_to_aircraft_design_tools + tool_name + '/' + element) + # + # elif os.path.isfile(tool_directory + '/' + element): + # shutil.copyfile(tool_directory + '/' + element, path_to_aircraft_design_tools + tool_name + # + '/' + element) + # repo.git.add(path_to_aircraft_design_tools + tool_name + '/' + element) + # + # commit_message = tool_name + ' - This is an automatically generated commit for the ' \ + # 'update of the named tool.' + # + # else: + # shutil.copytree(tool_directory, path_to_aircraft_design_tools + tool_name) + # repo.git.add(tool_name) + # commit_message = tool_name + ' - This is an automatically generated commit for the initial ' \ + # 'integration of the named tool.' + + # call function to write user data from ui to configuration.json file of new tool + error_flag = write_json_file(self, path_to_tool_json_files, '', tool_name, group_name, False, True, + path_to_aircraft_design_tools) + + if error_flag: + repo_error = 3 + + # try: + # changed_files = repo.index.diff("HEAD") + # if changed_files: + # repo.git.commit('-m', commit_message) + # + # except OSError: + # error_flag = True + # repo_error = 4 + + return repo_error, error_flag diff --git a/installer/sub_functions/resource_path.py b/installer/sub_functions/resource_path.py new file mode 100644 index 0000000000000000000000000000000000000000..35e827450f58c43e86102e6828164932310c90b2 --- /dev/null +++ b/installer/sub_functions/resource_path.py @@ -0,0 +1,29 @@ +def resource_path(): + """ Function to get the absolute path to the installer added resources + + * The input string "relative_path" contains the relative path of the resource you want to use. + + :param relative_path: input string + :return path_to_resources: output string + """ + + ''' imports for python ''' + import sys + import os + import tempfile + status_flag = True + list_of_running_executables = [] + + # generate path from temporary executable directory + path_to_resources = tempfile.gettempdir() # Get temp directory + content_of_temp = os.listdir(path_to_resources) + + if getattr(sys, 'frozen', False): + # If the script is compiled, sys._MEIPASS gives the temp directory for PyInstaller + path_to_resources = sys._MEIPASS + else: + path_to_resources = os.environ.get("_MEIPASS", os.path.abspath(".")) + + path_to_resources = path_to_resources.replace(os.sep, '/') + + return path_to_resources, status_flag diff --git a/installer/sub_functions/retranslate_ui.py b/installer/sub_functions/retranslate_ui.py new file mode 100644 index 0000000000000000000000000000000000000000..78b50c0692503e2142bf95702b4cf23789cc3544 --- /dev/null +++ b/installer/sub_functions/retranslate_ui.py @@ -0,0 +1,75 @@ +def retranslate_ui(self, UNICADOworkflowInstaller): + """ Call function to retranslate the strings of the installer application text fields. + + :rtype: object + """ + + ''' imports for python ''' + import os + from PyQt5 import QtCore + + _translate = QtCore.QCoreApplication.translate + UNICADOworkflowInstaller.setWindowTitle(_translate("UNICADOworkflow_installer", "UNICADOworkflow installer")) + self.integration_button.setText(_translate("UNICADOworkflow_installer", "Tool Integration")) + self.next_button.setText(_translate("UNICADOworkflow_installer", "Next >")) + self.cancel_button.setText(_translate("UNICADOworkflow_installer", "Cancel")) + self.back_button.setText(_translate("UNICADOworkflow_installer", "< Back")) + self.update_button.setText(_translate("UNICADOworkflow_installer", "Update")) + self.uninstall_button.setText(_translate("UNICADOworkflow_installer", "Uninstall")) + self.welcome_text_label_1.setText(_translate("UNICADOworkflow_installer", + "Welcome to the UNICADOworkflow Setup Wizard")) + self.install_text_label_1.setText(_translate("UNICADOworkflow_installer", + "This Wizard will guide you through the installation of " + "UNICADOworkflow.")) + self.continue_text_label.setText(_translate("UNICADOworkflow_installer", "Click Next to continue.")) + self.install_text_label_2.setText(_translate("UNICADOworkflow_installer", + "Please note: This is an unofficial installer. " + "It is provided by UNICADO project team.")) + self.header_text_label_1.setText(_translate("UNICADOworkflow_installer", "Choose Install Location")) + self.header_text_label_2.setText(_translate("UNICADOworkflow_installer", + "Chose the folder in which to install UNICADOworkflow.")) + self.install_and_finish_panel_text_label_1.setText(_translate("UNICADOworkflow_installer", + "If you are sure you want to perform, " + "click Install.")) + self.install_and_finish_panel_text_label_2.setText(_translate("UNICADOworkflow_installer", + "After completing the installation, click Finish.")) + self.repository_path_text_label_2.setText(_translate("UNICADOworkflow_installer", " Repository Folder")) + self.repository_path_line_edit.setText(_translate("UNICADOworkflow_installer", os.path.expanduser("~"))) + self.repository_path_browse_button.setText(_translate("UNICADOworkflow_installer", "Browse...")) + self.repository_path_text_label_1.setText(_translate("UNICADOworkflow_installer", + "Please, select the path to the parent directory of the " + "UNICADO repositories by clicking Browse. Attention, " + "do not select a specific repository! Then, click Install to " + "start the installation.")) + self.install_path_text_label_1.setText(_translate("UNICADOworkflow_installer", "Setup will install the UNICADO " + "workflow in the following folder. " + "To install in a different folder, " + "click Browse and select another f" + "older. Attention, this cannot be " + "changed later! Then click Next to " + "select the repository folder.")) + self.install_path_line_edit.setText(_translate("UNICADOworkflow_installer", "C:\\Programs\\UNICADOworkflow")) + self.install_path_browse_button.setText(_translate("UNICADOworkflow_installer", "Browse...")) + self.install_path_text_label_2.setText(_translate("UNICADOworkflow_installer", "Disk space required: 1.75 GB")) + self.install_path_text_label_3.setText(_translate("UNICADOworkflow_installer", "Disk space available: enough")) + self.install_path_text_label_4.setText(_translate("UNICADOworkflow_installer", " Destination Folder")) + self.integration_text_label_0.setText(_translate("UNICADOworkflow_installer", "UNICADO")) + self.integration_text_label_1.setText(_translate("UNICADOworkflow_installer", "Welcome to the UNICADO module " + "integration tool.")) + self.integration_text_label_2.setText(_translate("UNICADOworkflow_installer", "Entered tool name does not match the" + " 'camelCase' convention.")) + self.integration_text_label_3.setText(_translate("UNICADOworkflow_installer", 'Attention: There is no executable ' + 'file with the entered tool name in ' + 'the selected directory. ' + 'Please change.')) + self.integration_text_label_4.setText(_translate("UNICADOworkflow_installer", "Entered group name does not match " + "the 'camelCase' convention.")) + self.integration_text_label_5.setText(_translate("UNICADOworkflow_installer", "")) + self.tool_name_panel_edit.setText(_translate("UNICADOworkflow_installer", "Please enter name of tool to be " + "integrated")) + self.tool_name_panel_edit_button.setText(_translate("UNICADOworkflow_installer", "OK")) + self.tool_path_panel_edit.setText(_translate("UNICADOworkflow_installer", "Click Browse to enter local tool path")) + self.group_name_panel_edit.setText(_translate("UNICADOworkflow_installer", "Please enter tool group name.")) + self.group_name_panel_edit_button.setText(_translate("UNICADOworkflow_installer", "OK")) + self.checkbox_panel_edit.setText(_translate("UNICADOworkflow_installer", "enter password")) + self.checkbox_panel_text_label_0.setText(_translate("UNICADOworkflow_installer", "")) diff --git a/installer/sub_functions/uninstall_steps.py b/installer/sub_functions/uninstall_steps.py new file mode 100644 index 0000000000000000000000000000000000000000..4641c2fc7b6ce3e40295b5d18b78dc497ae36c7c --- /dev/null +++ b/installer/sub_functions/uninstall_steps.py @@ -0,0 +1,249 @@ +# imports for python +import os +import json +import time +import shutil +import subprocess +from PyQt5 import QtCore +from PyQt5.QtCore import QCoreApplication + + +def uninstall_steps(self): + # handle ui objects for visibility + self.welcome_panel.setVisible(False) + self.header_panel.setVisible(True) + self.integration_panel.setVisible(True) + self.tool_name_panel.setVisible(False) + self.update_button.setVisible(False) + self.update_button.setEnabled(False) + self.uninstall_button.setVisible(False) + self.uninstall_button.setEnabled(False) + self.next_button.setVisible(True) + self.next_button.setEnabled(True) + self.next_button.setText('Uninstall') + self.cancel_button.setText('Cancel') + self.back_button.setVisible(True) + self.back_button.setEnabled(True) + self.integration_button.setVisible(False) + self.integration_button.setEnabled(False) + + # set description to text fields + self.header_text_label_1.setText('Welcome to the UNICADO uninstaller.') + self.header_text_label_2.setText('This tool will guide you through the uninstallation of UNICADO.') + self.integration_text_label_1.setText('If you really want to uninstall the UNICADO workflow, click "Uninstall" ' + 'or click "Back" to return to the previous page. \n' + 'Click "Cancel" to quit the uninstaller.') + + +def uninstall_unicado(self): + """ Call function to uninstall all UNICADO components of the working- and .rce-directory. + + :rtype: object + """ + + # set handles for ui visibility + self.integration_button.setVisible(False) + self.integration_button.setEnabled(False) + self.continue_text_label.setText('') + QCoreApplication.processEvents() + + # generate absolute path to the json files inside the .rce directory + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + path_to_tool_json_files = user_path_string + '.rce/default/integration/tools/common/' + + # check if the absolute path file is not existing + # -> if true: -> open UNICADO tool list file to generate absolute path file + if not os.path.isfile(path_to_tool_json_files + 'absolutPathToUNICADOInstallDirectory.txt'): + # check if the UNICADO tool list file is existing + # -> if true: -> open json files of each listed tool in UNICADO tool list file + if os.path.isfile(path_to_tool_json_files + 'toolListUNICADOworkflow.txt'): + workflow_tools = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'r') + # loop across all listed tools in UNICADO tool list file to read absolute path of UNICADO install directory + for tool in workflow_tools: + folder_name = tool.rstrip() + if os.path.isdir(path_to_tool_json_files + '/' + folder_name): + # try to open json tool file and read absolute path of UNICADO install directory + with open(path_to_tool_json_files + folder_name + '/configuration.json', 'r+') as jsonFile: + json_data = json.load(jsonFile) + launch_settings = json_data['launchSettings'] + install_path = launch_settings[0]['rootWorkingDirectory'] + string_position = install_path.find('UNICADOworkflow') + # check if the absolute path of UNICADO install directory exist + # -> if true: -> generate absolute path file and break the loop + if not (string_position == -1): + install_directory = install_path[:string_position] + 'UNICADOworkflow/' + if os.path.isdir(install_directory): + file_for_install_path = open(path_to_tool_json_files + + 'absolutPathToUNICADOInstallDirectory.txt', 'w') + file_for_install_path.write(install_directory) + file_for_install_path.close() + break + + workflow_tools.close() + + # generate Error message and show in installer application + self.welcome_panel.setVisible(True) + self.integration_panel.setVisible(False) + self.welcome_text_label_1.setText('Attention! An Error has occurred!') + self.install_text_label_1.setText('UNICADOworkflow was not uninstalled successfully!') + self.install_text_label_2.setText('Please re-run the uninstaller. ' + 'After that, UNICADOworkflow will be uninstalled successfully.') + self.uninstall_button.setVisible(False) + self.update_button.setVisible(False) + self.cancel_button.setText('Abort') + self.cancel_button.setVisible(True) + QCoreApplication.processEvents() + + # else condition: the absolute path file is existing -> read absolute path from file and start uninstall process + else: + install_folder = open(path_to_tool_json_files + 'absolutPathToUNICADOInstallDirectory.txt', 'r') + install_directory = install_folder.read() + install_folder.close() + last_character = install_directory[-1] + if not (last_character == '/'): + install_directory = install_directory + '/' + indices = [] + i = install_directory.find('/') + + # try to uninstall the installed UNICADO working version and all components + try: + self.welcome_panel.setVisible(True) + self.integration_panel.setVisible(False) + self.welcome_text_label_1.setText('Uninstallation in progress, please wait.') + self.install_text_label_1.setText('Please wait until uninstallation is finished.') + self.install_text_label_2.setText('') + self.update_button.setVisible(False) + self.next_button.setVisible(False) + self.back_button.setVisible(False) + self.uninstall_button.setEnabled(False) + self.cancel_button.setVisible(True) + self.cancel_button.setEnabled(False) + self.cancel_button.setText('Finish') + QCoreApplication.processEvents() + + # Check if the 'unicado_own_python_packages.txt' file exists + if os.path.isfile(path_to_tool_json_files + 'unicado_own_python_packages.txt'): + # Check if the 'unicado_python_path.txt' file exists + if os.path.isfile(path_to_tool_json_files + 'unicado_python_path.txt'): + try: + # Read the Python interpreter path + with open(path_to_tool_json_files + 'unicado_python_path.txt', 'r') \ + as python_path_file: + python_path = python_path_file.read().strip() + + # Read and process the list of own packages + with open(path_to_tool_json_files + 'unicado_own_python_packages.txt', 'r') \ + as own_package_list: + for own_package in own_package_list: + own_package = own_package.strip() + if own_package: + with open(os.devnull, 'w') as devnull: + # Windows-specific console supress + if os.name == "nt": + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + subprocess.check_call([python_path, "-m", "pip", "uninstall", + own_package, '--yes'], stdout=devnull, + stderr=devnull, startupinfo=startupinfo) + # Linux/macOS + else: + subprocess.check_call([python_path, "-m", "pip", "uninstall", + own_package, '--yes'], stdout=devnull, + stderr=devnull) + + # Remove package list and Python path files after uninstallation + os.remove(path_to_tool_json_files + 'unicado_own_python_packages.txt') + os.remove(path_to_tool_json_files + 'unicado_python_path.txt') + + except OSError as e: + print(f'Error: Unable to handle files related to own Python packages. Details: {e}') + except subprocess.CalledProcessError as e: + print(f'Error: Failed to uninstall a package. Details: {e}') + else: + print('Error: The file "unicado_python_path.txt" does not exist. Cannot proceed.') + else: + print('Error: The file "unicado_own_python_packages.txt" does not exist. Cannot proceed.') + + while i >= 0: + indices.append(i) + i = install_directory.find('/', i + 1) + parent_folder = install_directory[:indices[-2]] + if os.path.isdir(install_directory): + workflow_tools = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'r') + # loop across all listed tools in the tool list file to remove tool from system + for tool in workflow_tools: + folder_name = tool.rstrip() + if os.path.isdir(install_directory + folder_name): + shutil.rmtree(install_directory + folder_name) + + # check if the installation directory is empty -> if true: -> delete empty directory + if not os.listdir(install_directory): + shutil.rmtree(install_directory) + + # check if the parent directory of install directory is empty -> if true: -> delete parent directory + if not os.listdir(parent_folder): + shutil.rmtree(parent_folder) + + # remove all json files of installed tools from .rce directory + workflow_tools = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'r') + for tool in workflow_tools: + folder_name = tool.rstrip() + if os.path.isdir(path_to_tool_json_files + '/' + folder_name): + shutil.rmtree(path_to_tool_json_files + folder_name) + + workflow_tools.close() + + # remove all UNICADO specific files from .rce directory + os.remove(path_to_tool_json_files + 'toolListUNICADOworkflow.txt') + os.remove(path_to_tool_json_files + 'absolutPathToUNICADOInstallDirectory.txt') + if os.path.isfile(path_to_tool_json_files + 'absolutPathToUNICADOrepositoryDirectory.txt'): + os.remove(path_to_tool_json_files + 'absolutPathToUNICADOrepositoryDirectory.txt') + if os.path.isfile(path_to_tool_json_files + 'standAloneInstallationFlag.dat'): + os.remove(path_to_tool_json_files + 'standAloneInstallationFlag.dat') + + # check if the standard tool file is existing -> if true: -> delete the file + if os.path.isfile(path_to_tool_json_files + 'standardToolListUNICADOworkflow.txt'): + os.remove(path_to_tool_json_files + 'standardToolListUNICADOworkflow.txt') + + # check if a standalone origin design tool directory is existing -> if true: -> delete the directory + if os.path.isdir(path_to_tool_json_files + 'originUnicadoDesignTools'): + shutil.rmtree(path_to_tool_json_files + 'originUnicadoDesignTools') + + # check if a standalone origin software tool directory is existing -> if true: -> delete the directory + if os.path.isdir(path_to_tool_json_files + 'originUnicadoSoftwareTools'): + shutil.rmtree(path_to_tool_json_files + 'originUnicadoSoftwareTools') + + # check if a standalone origin software tool directory is existing -> if true: -> delete the directory + if os.path.isdir(path_to_tool_json_files + 'originUnicadoProjects'): + shutil.rmtree(path_to_tool_json_files + 'originUnicadoProjects') + + # update the UNICADO installer application and show finished text + time.sleep(2) + self.welcome_text_label_1.setText('UNICADOworkflow was successfully uninstalled') + self.install_text_label_1.setText('Please click Finish to exit the installer.') + self.install_text_label_2.setText('') + self.uninstall_button.setVisible(False) + self.cancel_button.setVisible(True) + self.cancel_button.setEnabled(True) + QCoreApplication.processEvents() + + # exception handling if an error occurs during the uninstallation process + except OSError as e: + self.welcome_panel.setVisible(True) + self.integration_panel.setVisible(False) + self.welcome_text_label_1.setText('ERROR! Access denied!') + self.install_text_label_1.setGeometry(QtCore.QRect(30, 90, 400, 90)) + if ".metadata//.lock" in str(e).replace(os.sep, '/'): + self.install_text_label_1.setText("RCE is still running. Please close RCE before uninstallation.") + else: + self.install_text_label_1.setText(f"Attention: {str(e).replace(os.sep, '/')}") + self.continue_text_label.setText('To quit the installer, click Abort.') + self.next_button.setVisible(False) + self.back_button.setVisible(False) + self.uninstall_button.setVisible(False) + self.cancel_button.setEnabled(True) + self.cancel_button.setText('Abort') + self.cancel_button.setVisible(True) + QCoreApplication.processEvents() diff --git a/installer/sub_functions/update_additional_software.py b/installer/sub_functions/update_additional_software.py new file mode 100644 index 0000000000000000000000000000000000000000..c306fe2f80b64ad9209a06006ec4e99cecfd812c --- /dev/null +++ b/installer/sub_functions/update_additional_software.py @@ -0,0 +1,119 @@ +def update_additional_software(self, path_of_working_directory, path_to_origin, file_name, install_percent, percent): + """ Function to auto update the additional software tools in the UNICADO working directory. + + The input string "path_of_working_directory" contains the absolute path to installed working directory of + UNICADO. + + The input string "path_to_origin" contains the absolute system path to the repository of additional software. + + The input string "fileName" contains the folder name of aircraft projects. + + The input float "install_percent" contains the current value of install progress bar in percent. + + The input float "percent" contains the delta increment of install progress bar in percent. + + The output float "install_percent" contains the current value of install progress bar in percent after updating + the bar. + + :param self: input ui object + :param path_of_working_directory: input string + :param path_to_origin: input string + :param file_name: input string + :param install_percent: input float + :param percent: input float + :return install_percent: output float + """ + + ''' imports for python ''' + import os + import shutil + from PyQt5.QtCore import QCoreApplication + + # check for origin tool data and update only this files in working directory + if os.path.isdir(path_to_origin + file_name): + files_in_origin_tool_directory = os.listdir(path_to_origin + file_name) + + for data in files_in_origin_tool_directory: + # check if data a file, then replace with file from origin tool directory + if os.path.isfile(path_of_working_directory + file_name + '/' + data): + os.remove(path_of_working_directory + file_name + '/' + data) + shutil.copyfile(path_to_origin + file_name + '/' + data, path_of_working_directory + file_name + '/' + + data) + # update the installation progress bar + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # check if data a directory, then replace with directory from origin tool directory + if os.path.isdir(path_of_working_directory + file_name + '/' + data): + shutil.rmtree(path_of_working_directory + file_name + '/' + data) + # check if data is not equal to src or convertUNICADO2CPACS + # -> if true: -> copy data from origin to working directory + if not data == 'src' and not data == 'convertUNICADO2CPACS': + shutil.copytree(path_to_origin + file_name + '/' + data, path_of_working_directory + file_name + + '/' + data) + # update the installation progress bar + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # check if data is equal to convertUNICADO2CPACS + # -> if true: -> copy all content inside the origin directory out of the src directory to working copy + if data == 'convertUNICADO2CPACS': + data_inside = os.listdir(path_to_origin + file_name + '/' + data) + os.mkdir(path_of_working_directory + file_name + '/' + data) + for element in data_inside: + if not element == 'src': + if os.path.isfile(path_to_origin + file_name + '/' + data + '/' + element): + shutil.copyfile(path_to_origin + file_name + '/' + data + '/' + element, + path_of_working_directory + file_name + '/' + data + '/' + element) + else: + shutil.copytree(path_to_origin + file_name + '/' + data + '/' + element, + path_of_working_directory + file_name + '/' + data + '/' + element) + # update the installation progress bar + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # check if data exist in working directory, if not, then copy data to working directory + if not os.path.exists(path_of_working_directory + file_name + '/' + data): + if os.path.isfile(path_to_origin + file_name + '/' + data): + shutil.copyfile(path_to_origin + file_name + '/' + data, path_of_working_directory + file_name + + '/' + data) + # update the installation progress bar + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # check if data a directory, then replace with directory from origin tool directory + if os.path.isdir(path_to_origin + file_name + '/' + data): + # check if data is not equal to src or convertUNICADO2CPACS + # -> if true: -> copy data from origin to working directory + if not data == 'src' and not data == 'convertUNICADO2CPACS': + shutil.copytree(path_to_origin + file_name + '/' + data, path_of_working_directory + file_name + + '/' + data) + # update the installation progress bar + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # check if data is equal to convertUNICADO2CPACS + # -> if true: -> copy all content inside the origin directory out of the src directory + # to working copy + if data == 'convertUNICADO2CPACS': + data_inside = os.listdir(path_to_origin + file_name + '/' + data) + os.mkdir(path_of_working_directory + file_name + '/' + data) + for element in data_inside: + if not element == 'src': + if os.path.isfile(path_to_origin + file_name + '/' + data + '/' + element): + shutil.copyfile(path_to_origin + file_name + '/' + data + '/' + element, + path_of_working_directory + file_name + '/' + data + '/' + element) + else: + shutil.copytree(path_to_origin + file_name + '/' + data + '/' + element, + path_of_working_directory + file_name + '/' + data + '/' + element) + # update the installation progress bar + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + return install_percent diff --git a/installer/sub_functions/update_design_tools.py b/installer/sub_functions/update_design_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..6453ef8b1f8855c1de937c4c7d56cbf794590e2a --- /dev/null +++ b/installer/sub_functions/update_design_tools.py @@ -0,0 +1,79 @@ +def update_design_tools(self, path_of_working_directory, path_to_origin, file_name, install_percent, percent): + """ Function to auto update the additional software tools in the UNICADO working directory. + + The input string "path_of_working_directory" contains the absolute path to installed working directory + of UNICADO. + + The input string "path_to_origin" contains the absolute system path to the repository of additional software. + + The input string "fileName" contains the folder name of aircraft projects. + + The input float "install_percent" contains the current value of install progress bar in percent. + + The input float "percent" contains the delta increment of install progress bar in percent. + + The output float "install_percent" contains the current value of install progress bar in percent after updating + the bar. + + :param self: input ui object + :param path_of_working_directory: input string + :param path_to_origin: input string + :param file_name: input string + :param install_percent: input float + :param percent: input float + :return install_percent: output float + """ + + ''' imports for python ''' + import os + import shutil + from PyQt5.QtCore import QCoreApplication + + # check for origin tool data and update only this files in working directory + if os.path.isdir(path_to_origin + file_name): + files_in_origin_tool_directory = os.listdir(path_to_origin + file_name) + + for data in files_in_origin_tool_directory: + # check if data a file, then replace with file from origin tool directory + if os.path.isfile(path_of_working_directory + file_name + '/' + data): + os.remove(path_of_working_directory + file_name + '/' + data) + shutil.copyfile(path_to_origin + file_name + '/' + data, path_of_working_directory + file_name + + '/' + data) + # update the installation progress bar + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # check if data a directory, then replace with directory from origin tool directory + if os.path.isdir(path_of_working_directory + file_name + '/' + data): + shutil.rmtree(path_of_working_directory + file_name + '/' + data) + # check if data is not equal to src -> if true: -> copy data from origin to working directory + if not data == 'src': + shutil.copytree(path_to_origin + file_name + '/' + data, path_of_working_directory + file_name + + '/' + data) + # update the installation progress bar + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # check if data exist in working directory, if not, then copy data to working directory + if not os.path.exists(path_of_working_directory + file_name + '/' + data): + if os.path.isfile(path_to_origin + file_name + '/' + data): + shutil.copyfile(path_to_origin + file_name + '/' + data, path_of_working_directory + file_name + + '/' + data) + # update the installation progress bar + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + if os.path.isdir(path_to_origin + file_name + '/' + data): + # check if data is not equal to src -> if true: -> copy data from origin to working directory + if not data == 'src': + shutil.copytree(path_to_origin + file_name + '/' + data, path_of_working_directory + file_name + + '/' + data) + # update the installation progress bar + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + return install_percent diff --git a/installer/sub_functions/update_projects_folder.py b/installer/sub_functions/update_projects_folder.py new file mode 100644 index 0000000000000000000000000000000000000000..59eece31c663bb48c8ea5baa27bb78868bfaeb0d --- /dev/null +++ b/installer/sub_functions/update_projects_folder.py @@ -0,0 +1,54 @@ +def update_projects_folder(path_of_working_directory, path_to_aircraft_projects, file_name): + """ Function to auto update the additional software tools in the UNICADO working directory. + + The input string "path_of_working_directory" contains the absolute path to installed working directory + of UNICADO. + + The input string "path_to_aircraft_projects" contains the absolute system path to the repository of additional + software. + + The input string "fileName" contains the folder name of aircraft projects. + + :param path_of_working_directory: input string + :param path_to_aircraft_projects: input string + :param file_name: input string + :return none + """ + + ''' imports for python ''' + import os + import shutil + + ''' loop across all existing aircraft projects in current project folder ''' + files_in_origin_reference_directory = os.listdir(path_to_aircraft_projects) + for project in files_in_origin_reference_directory: + if not project == '.git': + # check if aircraft reference project exist in projects working directory + # -> if false: -> then copy aircraft reference project to working directory + if not os.path.exists(path_of_working_directory + file_name + '/' + project): + if os.path.isdir(path_to_aircraft_projects + project): + shutil.copytree(path_to_aircraft_projects + project, path_of_working_directory + file_name + '/' + + project) + else: + files_in_origin_reference_project_directory = os.listdir(path_to_aircraft_projects + project) + for data in files_in_origin_reference_project_directory: + # check if data a file, then replace with file from origin tool directory + if os.path.isfile(path_of_working_directory + file_name + '/' + project + '/' + data): + os.remove(path_of_working_directory + file_name + '/' + project + '/' + data) + shutil.copyfile(path_to_aircraft_projects + project + '/' + data, path_of_working_directory + + file_name + '/' + project + '/' + data) + + # check if data a directory, then replace with directory from origin tool directory + if os.path.isdir(path_of_working_directory + file_name + '/' + project + '/' + data): + shutil.rmtree(path_of_working_directory + file_name + '/' + project + '/' + data) + shutil.copytree(path_to_aircraft_projects + project + '/' + data, path_of_working_directory + + file_name + '/' + project + '/' + data) + + # check if data exist in working directory, if not, then copy data to working directory + if not os.path.exists(path_of_working_directory + file_name + '/' + project + '/' + data): + if os.path.isfile(path_to_aircraft_projects + project + '/' + data): + shutil.copyfile(path_to_aircraft_projects + project + '/' + data, path_of_working_directory + + file_name + '/' + project + '/' + data) + if os.path.isdir(path_to_aircraft_projects + project + '/' + data): + shutil.copytree(path_to_aircraft_projects + project + '/' + data, path_of_working_directory + + file_name + '/' + project + '/' + data) diff --git a/installer/sub_functions/update_steps.py b/installer/sub_functions/update_steps.py new file mode 100644 index 0000000000000000000000000000000000000000..156de38469712c6ba92649d5c89266cf8b7b9fa8 --- /dev/null +++ b/installer/sub_functions/update_steps.py @@ -0,0 +1,431 @@ +# imports for python +import os +import json +import stat +import time +import shutil +import configparser +from PyQt5 import QtCore, QtGui +from PyQt5.QtCore import QCoreApplication +from sub_functions.update_design_tools import update_design_tools +from sub_functions.update_projects_folder import update_projects_folder +from sub_functions.update_additional_software import update_additional_software + + +def update_steps(self): + # handle ui objects for visibility + self.welcome_panel.setVisible(False) + self.header_panel.setVisible(True) + self.integration_panel.setVisible(True) + self.tool_name_panel.setVisible(False) + self.update_button.setVisible(False) + self.update_button.setEnabled(False) + self.uninstall_button.setVisible(False) + self.uninstall_button.setEnabled(False) + self.next_button.setVisible(True) + self.next_button.setEnabled(True) + self.next_button.setText('Update') + self.cancel_button.setText('Cancel') + self.back_button.setVisible(True) + self.back_button.setEnabled(True) + self.integration_button.setVisible(False) + self.integration_button.setEnabled(False) + + # set description to text fields + self.header_text_label_1.setText('Welcome to the UNICADO updater.') + self.header_text_label_2.setText('This tool will guide you through the update job of UNICADO.') + self.integration_text_label_1.setText('If you really want to update the UNICADO workflow, click "Update" ' + 'or click "Back" to return to the previous page. \n' + 'Click "Cancel" to quit the uninstaller.') + + +def update_unicado(self): + """ Call function to update all installed UNICADO components of the working directory. + + :rtype: object + """ + + ''' initialize public parameter ''' + repository_path = str() + install_path = str() + path_to_aircraft_design_tools = str() + path_to_aircraft_projects = str() + path_to_software_tools = str() + + ''' update installed working version of UNICADO ''' + self.install_and_finish_panel.setVisible(True) + self.header_panel.setVisible(True) + font = QtGui.QFont() + font.setPointSize(16) + font.setBold(True) + font.setWeight(75) + self.header_text_label_1.setFont(font) + self.header_text_label_1.setText('Update in progress, please wait.') + self.header_text_label_2.setText('') + self.header_text_label_1.setGeometry(QtCore.QRect(30, 22, 400, 50)) + font = QtGui.QFont() + font.setFamily("Calibri") + font.setPointSize(11) + font.setBold(False) + self.install_and_finish_panel_text_label_1.setText('Please wait until UNICADOworkflow update is finished.') + self.install_and_finish_panel_text_label_2.setText('After completing the update process, click Finish.') + QCoreApplication.processEvents() + self.uninstall_button.setVisible(False) + self.back_button.setVisible(False) + self.back_button.setEnabled(False) + self.next_button.setVisible(False) + self.next_button.setEnabled(False) + self.update_button.setEnabled(False) + self.cancel_button.setVisible(True) + self.cancel_button.setEnabled(False) + self.cancel_button.setText('Finish') + self.integration_button.setVisible(False) + self.integration_button.setEnabled(False) + QCoreApplication.processEvents() + user_path_string = os.path.expanduser("~") + user_path_string = user_path_string.replace(os.sep, '/') + user_path_string = user_path_string + '/' + path_to_tool_json_files = user_path_string + '.rce/default/integration/tools/common/' + + # check if toolListUNICADOworkflow.txt exist and readable + try: + error_flag = False + workflow_tools = open(path_to_tool_json_files + 'toolListUNICADOworkflow.txt', 'a') + + # check if absolutPathToUNICADOInstallDirectory.txt and readable + try: + read_install_path = open(path_to_tool_json_files + 'absolutPathToUNICADOInstallDirectory.txt', 'r') + install_path = read_install_path.read() + read_install_path.close() + + # exception handling if not absolutPathToUNICADOInstallDirectory.txt exist or is not readable + except OSError: + for tool in workflow_tools: + folder_name = tool.rstrip() + if os.path.isdir(path_to_tool_json_files + '/' + folder_name): + with open(path_to_tool_json_files + folder_name + '/configuration.json', 'r+') as jsonFile: + json_data = json.load(jsonFile) + launch_settings = json_data['launchSettings'] + install_path = launch_settings[0]['rootWorkingDirectory'] + string_position = install_path.find('UNICADOworkflow') + if not (string_position == -1): + install_directory = install_path[:string_position] + 'UNICADOworkflow/' + if os.path.isdir(install_directory): + file_for_install_path = open(path_to_tool_json_files + + 'absolutPathToUNICADOInstallDirectory.txt', 'w') + file_for_install_path.write(install_directory) + file_for_install_path.close() + break + + # check if absolutPathToUNICADOrepositoryDirectory.txt and readable + try: + read_repository_path = open(path_to_tool_json_files + 'absolutPathToUNICADOrepositoryDirectory.txt', 'r') + repository_path = read_repository_path.read() + read_repository_path.close() + + # exception handling if not absolutPathToUNICADOrepositoryDirectory.txt exist or is not readable + except OSError: + error_flag = True + self.welcome_panel.setVisible(True) + self.install_and_finish_panel.setVisible(False) + self.header_panel.setVisible(False) + self.welcome_text_label_1.setText('Attention! An Error has occurred!') + self.install_text_label_1.setText('UNICADOworkflow can not be updated!') + self.install_text_label_2.setText('Please re-install UNICADOworkflow. After that, UNICADO will ' + 'automatically be updated.') + self.update_button.setVisible(False) + self.update_button.setEnabled(True) + self.uninstall_button.setVisible(True) + self.cancel_button.setText('Abort') + self.cancel_button.setEnabled(True) + self.cancel_button.setVisible(True) + QCoreApplication.processEvents() + + if not error_flag: + # check if the standard tool file is existing -> if true: -> delete the file + if os.path.isfile(path_to_tool_json_files + 'standardToolListUNICADOworkflow.txt'): + os.remove(path_to_tool_json_files + 'standardToolListUNICADOworkflow.txt') + + # set repository directories to copy files + files = os.listdir(repository_path) + for fileName in files: + if os.path.isdir(repository_path + fileName): + if os.path.isdir(repository_path + fileName + '/.git'): + if os.path.isfile(repository_path + fileName + '/.git/config'): + config = configparser.ConfigParser() + config.read(repository_path + fileName + '/.git/config') + if config.has_section('remote "origin"'): + url = config['remote "origin"']['url'] + if url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rADDITIONALSOFTWARE.git': + path_to_software_tools = repository_path + fileName + '/' + + if url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rAircraftDesign.git': + path_to_aircraft_design_tools = repository_path + fileName + '/' + + if url == 'ssh://git@unicado.ilr.rwth-aachen.de:2222/source/rAIRCRAFTREFERENCES.git': + path_to_aircraft_projects = repository_path + fileName + '/' + + # tool exist in working directory, only origin tool files will be updated, no changes to own files + # -> create list of files in working directory + files_in_working_directory = os.listdir(install_path) + files_in_origin_tool_directory = os.listdir(path_to_aircraft_design_tools) + self.timer = QtCore.QTimer() + self.timer.start(0) + install_percent = float(0) + copy_count_working = len(files_in_working_directory) * 10 + copy_count_origin = len(files_in_origin_tool_directory) * 10 + if copy_count_working >= copy_count_origin: + percent = float(round(100 / copy_count_working, 1)) + else: + percent = float(round(100 / copy_count_origin, 1)) + + # loop for all elements in list of files + for fileName in files_in_working_directory: + if fileName == 'projects': + update_projects_folder(install_path, path_to_aircraft_projects, fileName) + # update the installation progress bar + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # check if current list element not 'workingDirectoryRCE' or 'workflowResults' + if not (fileName == 'workingDirectoryRCE') and not (fileName == 'workflowResults') \ + and not (fileName == 'projects'): + # change execution rights for each file and folder in 'filName' to read, write and execute + for root, dirs, files in os.walk(install_path + fileName): + for directory in dirs: + os.chmod(os.path.join(root, directory), stat.S_IRWXU) + for file in files: + os.chmod(os.path.join(root, file), stat.S_IRWXU) + + # path handling to origin tool directories + if (fileName == 'gnuplot') or (fileName == 'gnuplot-linux') or (fileName == 'inkscape') \ + or (fileName == 'inkscape-linux') or (fileName == 'reportGenerator') \ + or (fileName == 'cpacsInterface'): + path_to_origin = path_to_software_tools + # call function to update the installed additional software in the unicado working directory + install_percent = update_additional_software(self, install_path, path_to_origin, fileName, + install_percent, percent) + + else: + path_to_origin = path_to_aircraft_design_tools + # call function to update the installed aircraft design tools in the unicado working directory + install_percent = update_design_tools(self, install_path, path_to_origin, fileName, + install_percent, percent) + + # copy json files to .rce directory and set paths to module json file + json_files = os.listdir(path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles') + for jsonfile in json_files: + if not os.path.isdir(install_path + jsonfile): + if os.path.isdir(path_to_software_tools + jsonfile): + source = path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles/' + jsonfile + destination = path_to_tool_json_files + jsonfile + if not os.path.isdir(path_to_tool_json_files + jsonfile): + shutil.copytree(source, destination) + workflow_tools.write(jsonfile + '\n') + if not os.path.isdir(install_path + jsonfile): + shutil.copytree(path_to_software_tools + jsonfile, install_path + jsonfile) + # write paths to json configuration file of each module + with open(path_to_tool_json_files + jsonfile + '/configuration.json', 'r+') as jsonFile: + json_data = json.load(jsonFile) + launch_settings = json_data['launchSettings'] + launch_settings[0]['rootWorkingDirectory'] = install_path + 'workingDirectoryRCE' + launch_settings[0]['toolDirectory'] = install_path + jsonfile + json.dump(json_data, open(path_to_tool_json_files + jsonfile + '/configuration.json', 'w'), + indent=2, sort_keys=False) + elif os.path.isdir(path_to_aircraft_design_tools + jsonfile): + source = path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles/' + jsonfile + destination = path_to_tool_json_files + jsonfile + if not (os.path.isdir(path_to_tool_json_files + jsonfile)): + shutil.copytree(source, destination) + workflow_tools.write(jsonfile + '\n') + if not (os.path.isdir(install_path + jsonfile)): + shutil.copytree(path_to_aircraft_design_tools + jsonfile, install_path + jsonfile) + # write paths to json configuration file of each module + with open(path_to_tool_json_files + jsonfile + '/configuration.json', 'r+') as jsonFile: + json_data = json.load(jsonFile) + launch_settings = json_data['launchSettings'] + launch_settings[0]['rootWorkingDirectory'] = install_path + 'workingDirectoryRCE' + launch_settings[0]['toolDirectory'] = install_path + jsonfile + json.dump(json_data, open(path_to_tool_json_files + jsonfile + '/configuration.json', 'w'), + indent=2, sort_keys=False) + else: + source = path_to_aircraft_design_tools + 'UNICADOworkflow/jsonFiles/' + jsonfile \ + + '/configuration.json' + destination = path_to_tool_json_files + jsonfile + '/configuration.json' + shutil.copy(source, destination) + # write paths to json configuration file of each module + with open(path_to_tool_json_files + jsonfile + '/configuration.json', 'r+') as jsonFile: + json_data = json.load(jsonFile) + launch_settings = json_data['launchSettings'] + launch_settings[0]['rootWorkingDirectory'] = install_path + 'workingDirectoryRCE' + launch_settings[0]['toolDirectory'] = install_path + jsonfile + json.dump(json_data, open(path_to_tool_json_files + jsonfile + '/configuration.json', 'w'), + indent=2, sort_keys=False) + + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # copy workflow and workflow configuration file + if os.path.isdir(path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkFlowOnRCE/'): + list_of_elements = os.listdir(path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkFlowOnRCE/') + for element in list_of_elements: + source = path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkFlowOnRCE/' + element + destination = install_path + 'workingDirectoryRCE/UNICADOworkflow/' + if os.path.isfile(path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkFlowOnRCE/' + + element): + shutil.copy(source, destination) + if os.path.isdir(path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkFlowOnRCE/' + element): + if os.path.isdir(install_path + 'workingDirectoryRCE/UNICADOworkflow/src'): + shutil.rmtree(install_path + 'workingDirectoryRCE/UNICADOworkflow/src') + shutil.copytree(source, destination + 'src') + QCoreApplication.processEvents() + + # check for own files, if existing, copy to new working directory + if os.path.isdir(install_path + 'workingDirectoryRCE/UNICADOworkflow/UNICADOworkFlowOnRCE/'): + list_of_existing_elements = os.listdir( + install_path + 'workingDirectoryRCE/UNICADOworkflow/UNICADOworkFlowOnRCE/') + for existing_element in list_of_existing_elements: + source = install_path + 'workingDirectoryRCE/UNICADOworkflow/UNICADOworkFlowOnRCE/' + destination = install_path + 'workingDirectoryRCE/UNICADOworkflow/' + if not os.path.exists(install_path + 'workingDirectoryRCE/UNICADOworkflow/' + existing_element): + if os.path.isfile( + install_path + 'workingDirectoryRCE/UNICADOworkflow/UNICADOworkFlowOnRCE/' + + existing_element): + shutil.copyfile(source + existing_element, destination + existing_element) + if os.path.isdir( + install_path + 'workingDirectoryRCE/UNICADOworkflow/UNICADOworkFlowOnRCE/' + + existing_element): + shutil.copytree(source + existing_element, destination + existing_element) + QCoreApplication.processEvents() + shutil.rmtree(install_path + 'workingDirectoryRCE/UNICADOworkflow/UNICADOworkFlowOnRCE/') + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # move .metadata directory to workingDirectoryRCE + if os.path.isdir(install_path + 'workingDirectoryRCE/UNICADOworkflow/.metadata'): + if os.path.isdir(install_path + 'workingDirectoryRCE/.metadata'): + shutil.rmtree(install_path + 'workingDirectoryRCE/.metadata') + source = install_path + 'workingDirectoryRCE/UNICADOworkflow/.metadata' + destination = install_path + 'workingDirectoryRCE/.metadata' + shutil.copytree(source, destination) + shutil.rmtree(install_path + 'workingDirectoryRCE/UNICADOworkflow/.metadata') + QCoreApplication.processEvents() + + # copy workflow configuration file to workflow directory + if os.path.isfile(path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkflow_conf.xml'): + source = path_to_aircraft_design_tools + 'UNICADOworkflow/UNICADOworkflow_conf.xml' + destination = install_path + 'workingDirectoryRCE/UNICADOworkflow/UNICADOworkflow_conf.xml' + shutil.copy(source, destination) + + # copy version.txt to working directory of UNICADOworkflow + if os.path.isfile(path_to_aircraft_design_tools + 'UNICADOworkflow/version.txt'): + shutil.copyfile(path_to_aircraft_design_tools + 'UNICADOworkflow/version.txt', + install_path + 'workingDirectoryRCE/UNICADOworkflow/workingVersion.txt') + + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # write paths to json configuration file of each module + path_to_settings_file = user_path_string + '.rce/default/internal/' + if not os.path.isdir(user_path_string + '.rce/default/internal/'): + os.makedirs(user_path_string + '.rce/default/internal/') + + if not os.path.isfile(user_path_string + '.rce/default/internal/settings.json'): + json_data = {'rce.workspace.recentLocations': '', + 'rce.workspace.lastLocation': '', + 'rce.workspace.dontAskAgain': 'false'} + # Write JSON file + with open(user_path_string + '.rce/default/internal/settings.json', 'w') as jsonFile: + json.dump(json_data, open(path_to_settings_file + 'settings.json', 'w')) + + if os.path.isfile(user_path_string + '.rce/default/internal/settings.json'): + with open(path_to_settings_file + 'settings.json', 'r+') as jsonFile: + # check if settings.json file is empty + try: + json_data = json.load(jsonFile) + except OSError: + json_data = {'rce.workspace.recentLocations': '', + 'rce.workspace.lastLocation': '', + 'rce.workspace.dontAskAgain': 'false'} + workspace_location_rce = json_data['rce.workspace.recentLocations'] + if os.name == 'nt': + install_path = install_path.replace('/', '\\') + alternate_path = install_path.replace('\\', '\\\\') + json_data['rce.workspace.lastLocation'] = install_path + 'workingDirectoryRCE' + if not install_path[0] + '\:' + '\\' + install_path[3:] \ + + 'workingDirectoryRCE' in workspace_location_rce: + if not install_path[0] + '\:' + alternate_path[2:] + 'workingDirectoryRCE' \ + in workspace_location_rce: + json_data['rce.workspace.recentLocations'] = install_path[0] + '\:' + alternate_path[2:] \ + + 'workingDirectoryRCE' + ':' \ + + workspace_location_rce + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + else: + str_start = workspace_location_rce.index(install_path[0] + '\:' + alternate_path[2:] + + 'workingDirectoryRCE') + str_end = workspace_location_rce.index('workingDirectoryRCE') + if str_start > 0: + sub_string_front = workspace_location_rce[0:str_start - 1] + total_length = len(workspace_location_rce) + new_string = workspace_location_rce[str_start:str_end + 19] + ':' + sub_string_front + if str_end + 19 < total_length: + sub_string_end = workspace_location_rce[str_end + 20:total_length] + new_string = new_string + ':' + sub_string_end + json_data['rce.workspace.recentLocations'] = new_string + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + if os.name == 'posix': + json_data['rce.workspace.lastLocation'] = install_path + 'workingDirectoryRCE' + if not install_path + 'workingDirectoryRCE' in workspace_location_rce: + json_data['rce.workspace.recentLocations'] = install_path + 'workingDirectoryRCE' + ':' \ + + workspace_location_rce + install_percent += percent + self.install_progress_bar.setValue(int(install_percent)) + QCoreApplication.processEvents() + + # check if rce.workspace.dontAskAgain exist and set to false + if 'rce.workspace.dontAskAgain' in json_data: + json_data['rce.workspace.dontAskAgain'] = 'false' + else: + json_data['rce.workspace.dontAskAgain'] = 'false' + + json.dump(json_data, open(path_to_settings_file + 'settings.json', 'w')) + + self.install_progress_bar.setValue(int(100)) + QCoreApplication.processEvents() + time.sleep(1) + self.welcome_text_label_1.setText('UNICADOworkflow was successfully updated') + self.install_text_label_1.setText('Please click Finish to exit the installer.') + self.install_text_label_2.setText('') + self.update_button.setVisible(False) + self.cancel_button.setVisible(True) + self.cancel_button.setEnabled(True) + QCoreApplication.processEvents() + + workflow_tools.close() + + # exception handling if not toolListUNICADOworkflow.txt exist or is not readable + except OSError: + self.welcome_panel.setVisible(True) + self.install_and_finish_panel.setVisible(False) + self.header_panel.setVisible(False) + self.welcome_text_label_1.setText('Attention! An Error has occurred!') + self.install_text_label_1.setText('UNICADOworkflow can not be updated!') + self.install_text_label_2.setText('Please re-install UNICADOworkflow. ' + 'After that, UNICADO will automatically be updated.') + self.update_button.setVisible(False) + self.update_button.setEnabled(True) + self.uninstall_button.setVisible(True) + self.cancel_button.setText('Abort') + self.cancel_button.setEnabled(True) + self.cancel_button.setVisible(True) + QCoreApplication.processEvents() diff --git a/installer/sub_functions/write_json_file.py b/installer/sub_functions/write_json_file.py new file mode 100644 index 0000000000000000000000000000000000000000..c8ce4fe6522a8ff1c0f37896030a50ef4dfd8e19 --- /dev/null +++ b/installer/sub_functions/write_json_file.py @@ -0,0 +1,104 @@ +# imports for python +import os +#import git +import json + + +def write_json_file(self, path_to_tool_json_files, install_path, tool_name, group_name, error_flag, integration_flag, + path_to_aircraft_design_tools): + # initialize local parameter + error_handling_string = str("") + + try: + if self.integration_radio_button_yes.isChecked(): + error_handling_string = "import os\r\ncurrent_working_dir = \"${in:current_workflow_name}\"\r\n\r\nif ${addProp:exitCode} == 1:\r\n\t### read sys path for python scripts\r\n\tuser_path_string = os.path.expanduser(\"~\")\r\n\r\n\t## convert path of curent working directory to a python path -> \\ to /\r\n\tuser_path_string = user_path_string.replace(os.sep, '/')\r\n\r\n\t## generate sys paths from file in .rce-directory\r\n\tinstall_path = open(user_path_string + '/.rce/default/integration/tools/common/absolutPathToUNICADOInstallDirectory.txt','r')\r\n\tinstall_path_directory = str(install_path.read())\r\n\tinstall_path_directory = install_path_directory.replace(os.sep, '/')\r\n\r\n\t## path to working directory of rce \r\n\tpath_of_working_directory_rce = install_path_directory + 'workingDirectoryRCE/' + current_working_dir\r\n\r\n\t## write error data to system\r\n\terror_dat = open(path_of_working_directory_rce + '/temp/workflowExecutionError.dat', 'a+')\r\n\terror_dat.close()\r\n\t\r\n\t## write tool name to tool error list of current workflow loop\r\n\ttool_error_list = open(path_of_working_directory_rce + '/temp/toolErrorList.log', 'a+')\r\n\ttool_error_list.write('Error occurred in design tool: calculatePolar' + '\\n')\r\n\ttool_error_list.close()\r\n\r\n${out:current_workflow_name} = current_working_dir\r\n" + + if self.integration_radio_button_no.isChecked(): + error_handling_string = "import os\r\ncurrent_working_dir = \"${in:current_workflow_name}\"\r\n\r\nif ${addProp:exitCode} == 1:\r\n\t### read sys path for python scripts\r\n\tuser_path_string = os.path.expanduser(\"~\")\r\n\r\n\t## convert path of curent working directory to a python path -> \\ to /\r\n\tuser_path_string = user_path_string.replace(os.sep, '/')\r\n\r\n\t## generate sys paths from file in .rce-directory\r\n\tinstall_path = open(user_path_string + '/.rce/default/integration/tools/common/absolutPathToUNICADOInstallDirectory.txt','r')\r\n\tinstall_path_directory = str(install_path.read())\r\n\tinstall_path_directory = install_path_directory.replace(os.sep, '/')\r\n\r\n\t## path to working directory of rce \r\n\tpath_of_working_directory_rce = install_path_directory + 'workingDirectoryRCE/' + current_working_dir\r\n\r\n\t## write tool name to tool error list of current workflow loop\r\n\ttool_error_list = open(path_of_working_directory_rce + '/temp/toolErrorList.log', 'a+')\r\n\ttool_error_list.write('Error occurred in design tool: calculateEmissions' + '\\n')\r\n\ttool_error_list.close()\r\n\r\n${out:current_workflow_name} = current_working_dir\r\n" + + if not os.path.isdir(path_to_tool_json_files + tool_name): + os.mkdir(path_to_tool_json_files + tool_name) + + configuration_dict = { + "commandScriptLinux": './' + tool_name + '-c ../workingDirectoryRCE/${in:current_workflow_name}/' + + tool_name + '_conf.xml', + "commandScriptWindows": tool_name + '.exe -c ../workingDirectoryRCE/${in:current_workflow_name}/' + + tool_name + '_conf.xml', + "copyToolBehavior": "never", + "deleteWorkingDirectoriesAfterWorkflowExecution": True, + "documentationFilePath": "", + "dontCrashOnNonZeroExitCodes": True, + "enableCommandScriptLinux": True, + "enableCommandScriptWindows": True, + "groupName": group_name, + "imitationScript": "", + "imitationToolOutputFilename": "", + "inputs": [{ + "inputHandling": "Queue", + "endpointFileName": "", + "endpointDataType": "ShortText", + "defaultInputExecutionConstraint": "Required", + "endpointName": 'current_workflow_name', + "defaultInputHandling": "Queue", + "inputExecutionConstraint": "Required", + "endpointFolder": "" + }], + "integrationType": "Common", + "isActive": True, + "launchSettings": [{ + "limitInstallationInstancesNumber": "10", + "limitInstallationInstances": "true", + "rootWorkingDirectory": install_path + 'workingDirectoryRCE', + "host": "RCE", + "toolDirectory": install_path + tool_name, + "version": "allTime" + }], + "outputs": [{ + "inputHandling": "-", + "endpointFileName": "", + "endpointDataType": "Boolean", + "endpointName": 'outputFlag' + tool_name[0].upper() + tool_name[1:], + "inputExecutionConstraint": "-", + "endpointFolder": "" + }, { + "inputHandling": "-", + "endpointFileName": "", + "endpointDataType": "ShortText", + "endpointName": "current_workflow_name", + "inputExecutionConstraint": "-", + "endpointFolder": "" + }], + "postScript": error_handling_string, + "preScript": "", + "setToolDirAsWorkingDir": True, + "toolDescription": "", + "toolIconPath": "", + "toolIntegrationVersion": 1, + "toolIntegratorE-Mail": "", + "toolIntegratorName": "", + "toolName": tool_name, + "toolProperties": { + "Default": {} + }, + "uploadIcon": True + } + + json.dump(configuration_dict, open(path_to_tool_json_files + tool_name + '/configuration.json', 'w'), + indent=2, sort_keys=False) + + # if integration_flag: + # with open(path_to_tool_json_files + tool_name + '/configuration.json', 'r+') as jsonFile: + # json_dict = json.load(jsonFile) + # json_dict['launchSettings'][0]['rootWorkingDirectory'] = 'C:/' + # json_dict['launchSettings'][0]['toolDirectory'] = 'C:/' + # json.dump(json_dict, open(path_to_tool_json_files + tool_name + '/configuration.json', 'w'), + # indent=2, sort_keys=False) + # repo = git.Repo(path_to_aircraft_design_tools) + # repo.git.add(path_to_tool_json_files + tool_name + '/configuration.json') + + # exception handling for json error -> if error: -> set error flag to true for display error message in ui + except OSError: + error_flag = True + return error_flag + + return error_flag diff --git a/installer/sub_functions/write_path_to_environment.py b/installer/sub_functions/write_path_to_environment.py new file mode 100644 index 0000000000000000000000000000000000000000..015e88bdbf89fb3a9730d70df4585ddb9d5481a2 --- /dev/null +++ b/installer/sub_functions/write_path_to_environment.py @@ -0,0 +1,88 @@ +def write_path_to_environment(path_to_add_to_environment): + """ + Function to write the entered path permanently to the local user environment. + + * The input string 'path_to_add_to_environment' contains the path to add. + + * The output bool 'status_path_adding' contains the status whether adding paths was successful. + + :param path_to_add_to_environment: input string + :return status_path_adding: output bool + """ + ''' imports for python ''' + import os + import winreg + import unicodedata + + ''' initialize local parameter ''' + count = 0 + index_1 = 0 + path_new = str() + status_path_adding = True + + # check if the operating system is Windows -> if true: -> try to write local user path environment + if os.name == 'nt': + ''' convert entered string to windows norm path ''' + path_to_add_to_environment = os.path.normpath(path_to_add_to_environment) + + ''' read existing path variables from users local registry ''' + # try to open environment path variable to read existing local path entries + try: + key_q = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Environment', 0, winreg.KEY_QUERY_VALUE) + path_old, _ = winreg.QueryValueEx(key_q, "PATH") + winreg.CloseKey(key_q) + + # convert registry key value to ascii string containing the existing path variables + path_old = str(unicodedata.normalize('NFKD', path_old).encode('ascii', 'ignore')) + + # loop across all characters of string to cut unnecessary elements at the beginning of converted string + for char in path_old: + if not char == 'C' or char == '%': + path_old = path_old[1:] + else: + break + + # check if the last character of converted string equal to ' -> if true: -> delete character from string + if path_old[-1] == "'": + path_old = path_old[:-1] + + # loop across all characters of loaded string to convert containing path elements to windows norm paths + for chars in path_old: + if chars == ';': + index_2 = count + single_path = path_old[index_1:index_2] + index_1 = index_2 + 1 + single_path = os.path.normpath(single_path) + path_new = path_new + ';' + single_path + + count += 1 + + # exception handling if the local environment variables could not read or does not exist + except OSError: + status_path_adding = False + return status_path_adding + + # check if the entered path already exist in the local user environment variables + # -> if true: -> do nothing and return + if not str(path_new).find(str(path_to_add_to_environment)) == -1: + return status_path_adding + + # else condition: entered path currently does not exist in the local user environment variables -> add path + else: + path_new = path_new[1:] + ';' + path_to_add_to_environment + # try to open environment path variable to write new local path entries + try: + registry_key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Environment', 0, winreg.KEY_WRITE) + winreg.SetValueEx(registry_key, "PATH", 0, winreg.REG_SZ, path_new) + winreg.CloseKey(registry_key) + + # exception handling if the paths could not write to the local user environment variables + except OSError: + status_path_adding = False + return status_path_adding + + # check if the operating system is unix or linux -> if true: -> send False to caller function + if os.name == 'posix': + # add path to local user environment variables for unix or linx currently not implemented + status_path_adding = False + return status_path_adding diff --git a/installer/unicadoICON.svg b/installer/unicadoICON.svg new file mode 100644 index 0000000000000000000000000000000000000000..08022fe96a92b816be749cb6715a4c88d7f11b33 --- /dev/null +++ b/installer/unicadoICON.svg @@ -0,0 +1 @@ +<svg width="38" height="38" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><defs><clipPath id="clip0"><rect x="618" y="266" width="38" height="38"/></clipPath><clipPath id="clip1"><rect x="619" y="267" width="36" height="36"/></clipPath><clipPath id="clip2"><rect x="619" y="267" width="36" height="36"/></clipPath><clipPath id="clip3"><rect x="619" y="268" width="35" height="34"/></clipPath><clipPath id="clip4"><rect x="619" y="268" width="35" height="34"/></clipPath><clipPath id="clip5"><rect x="619" y="268" width="35" height="34"/></clipPath><linearGradient x1="113.319" y1="135.713" x2="98.8327" y2="150.199" gradientUnits="userSpaceOnUse" spreadMethod="pad" id="fill6"><stop offset="0" stop-color="#6C6C6C"/><stop offset="1"/></linearGradient><linearGradient x1="107.198" y1="144.648" x2="87.9471" y2="148.629" gradientUnits="userSpaceOnUse" spreadMethod="pad" id="fill7"><stop offset="0" stop-color="#00427D"/><stop offset="1" stop-color="#007DEC"/></linearGradient><linearGradient x1="92.4649" y1="130.931" x2="102.156" y2="143.93" gradientUnits="userSpaceOnUse" spreadMethod="pad" id="fill8"><stop offset="0" stop-color="#E20E1F"/><stop offset="1" stop-color="#F24657"/></linearGradient><linearGradient x1="120.201" y1="135.561" x2="100.544" y2="130.504" gradientUnits="userSpaceOnUse" spreadMethod="pad" id="fill9"><stop offset="0" stop-color="#EA7D92"/><stop offset="1" stop-color="#A91B36" stop-opacity="0.890196"/></linearGradient><clipPath id="clip10"><rect x="628" y="268" width="16" height="17"/></clipPath><clipPath id="clip11"><rect x="628" y="268" width="16" height="17"/></clipPath><clipPath id="clip12"><rect x="628" y="268" width="16" height="17"/></clipPath><clipPath id="clip13"><rect x="-4.37975" y="-0.31867" width="113.361" height="119.065"/></clipPath><image width="54" height="59" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAARCAMAAAA11AaTAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAEIUExURQAAAP///4D//zPMzDnGxlW/1U7E2Ee4xlHF0TW1tSm1vWLL2CyotFrL1Teyuz+0xCynty+pu1C9zDawvi+ru1vH1WLM2CSjsySltCintVLDz2fR3WjR3V/K1zKtvD20wjqywGDL1ySks3XZ5Semtky/zHPZ5CWktCemtSmntiqotyuouCypuC2quS6quTCrujKtvDOuvTSvvjawvzexwDixwDuzwj61xD+2xEC3xUK4xkS5x0a7yEe7yUi8ykq+y0u+y02/zU7Bzk/BzlLCz1TE0VXF0lbG0ljH1FrI1VvJ1l3K11/M2GLN2mPO2mXP22bQ3GjS3mnS3mzU4G/W4nPZ5HTZ5Xbb5qHmlkQAAAAndFJOUwABAgUJDA0SFhgfJyk2OD1AR2l6g4yRv9nk5Oju8PLy9/j7/f7+/vx2s+EAAAAJcEhZcwAADsMAAA7DAcdvqGQAAACVSURBVChTNcnVGoJQEIXREbu7O7FbDOzuVt7/TYTDdt3Mv78hlSNRzbnQRJpkr92oFEOYenEodFv1Mh9kk1tMxYHQadZKvEXZkdV8MuqnvFpzOCNPw36znI2dyofcNqL4cbfOc2wS+Yiu58PWiCmz3y8nD1oReN7SSCb2euiQzPcdRamkjwmlkrIIkKwIKOD++XEZoh/pXhA98G3bzwAAAABJRU5ErkJggg==" preserveAspectRatio="none" id="img14"></image><clipPath id="clip15"><rect x="0" y="0" width="107" height="116.907"/></clipPath><linearGradient x1="122.418" y1="143.482" x2="111.163" y2="131.239" gradientUnits="userSpaceOnUse" spreadMethod="pad" id="fill16"><stop offset="0" stop-color="#204B78"/><stop offset="1" stop-color="#6199D4"/></linearGradient></defs><g clip-path="url(#clip0)" transform="translate(-618 -266)"><g clip-path="url(#clip1)"><g clip-path="url(#clip2)"><g clip-path="url(#clip3)"><g clip-path="url(#clip4)"><g clip-path="url(#clip5)"><path d="M113.319 135.713C113.319 135.713 116.611 141.998 112.065 147.272 106.397 153.848 98.8327 150.199 98.8327 150.199 98.8327 150.199 104.954 148.801 108.947 144.459 111.862 141.289 113.319 135.713 113.319 135.713Z" stroke="#161616" stroke-width="0.053232" fill="url(#fill6)" transform="matrix(1 0 0 1.01175 531.482 148.745)"/><path d="M107.198 144.648C107.198 144.648 97.6053 153.056 89.8972 147.196 85.6735 143.985 91.7984 136.93 91.7984 136.93 91.7984 136.93 88.5837 141.738 93.9277 144.497 97.8019 146.497 107.198 144.648 107.198 144.648Z" stroke="#444444" stroke-width="0.053232" fill="url(#fill7)" transform="matrix(1 0 0 1.01175 531.482 148.745)"/><path d="M96.9401 124.518C96.9401 124.518 95.2781 128.604 96.6712 135.057 97.7291 139.956 102.156 143.93 102.156 143.93 102.156 143.93 94.2052 143.765 92.4232 135.541 90.9391 128.692 96.9401 124.518 96.9401 124.518Z" stroke="#E30E1F" stroke-width="0.053232" stroke-opacity="0.984314" fill="url(#fill8)" transform="matrix(1 0 0 1.01175 531.482 148.745)"/><path d="M100.544 130.504C100.544 130.504 104.7 124.283 112.065 125.98 120.049 127.818 120.201 135.561 120.201 135.561 120.201 135.561 114.147 130.61 110.468 129.744 107.238 128.984 100.544 130.504 100.544 130.504Z" stroke="#C7314F" stroke-width="0.053232" stroke-opacity="0.952941" fill="url(#fill9)" transform="matrix(1 0 0 1.01175 531.482 148.745)"/><g clip-path="url(#clip10)"><g clip-path="url(#clip11)"><g clip-path="url(#clip12)"><g clip-path="url(#clip13)" transform="matrix(0.141142 0 0 0.142779 628.618 268.046)"><g clip-path="url(#clip15)" transform="matrix(1 0 0 1.00079 2.92156e-05 4.61083e-05)"><use width="100%" height="100%" xlink:href="#img14" transform="scale(1.98148 1.98148)"></use></g></g></g></g></g><path d="M111.163 131.239C111.163 131.239 122.555 134.865 122.374 143.258 122.2 151.395 112.158 148.5 112.158 148.5 112.158 148.5 117.647 148.85 117.826 143.37 117.993 138.255 111.163 131.239 111.163 131.239Z" stroke="#3D6EA2" stroke-width="0.053232" stroke-opacity="0.980392" fill="url(#fill16)" transform="matrix(1 0 0 1.01175 531.482 148.745)"/></g></g></g><path d="M30.1096-28.0873 30.1096 0.168524 22.4137 0.168524 22.4137-28.0873ZM18.3129-7.07801 21.0093-2.19081 21.0093-2.13464C19.0619-0.411947 16.6464 0.449397 13.7628 0.449397 9.19392 0.449397 5.95451-0.786445 4.04457-3.25813 2.28444-5.46767 1.40437-8.98794 1.40437-13.819L1.40437-28.0873 9.10029-28.0873 9.10029-13.819C9.10029-12.8078 9.11902-11.8341 9.15647-10.8979 9.23137-9.96164 9.43734-9.13774 9.77439-8.4262 10.1489-7.71465 10.7106-7.13418 11.4596-6.68478 12.2086-6.27284 13.2759-6.06686 14.6616-6.06686 15.2982-6.06686 15.9162-6.16049 16.5153-6.34773 17.1145-6.53498 17.6763-6.77841 18.2006-7.07801Z" fill="#BFBFBF" transform="matrix(1 0 0 1.0116 526.347 301.17)"/><path d="M22.4675-28.0873 30.1072-28.0873 30.1072 0 22.4675 0ZM48.4201-24.8292C49.3564-23.6683 50.0305-22.2077 50.4424-20.4476 50.8544-18.6874 51.0604-16.5153 51.0604-13.9313L51.0604 0 43.4206 0 43.4206-13.9313C43.4206-14.9799 43.3832-15.9723 43.3083-16.9086 43.2708-17.8823 43.0836-18.7249 42.7465-19.4364 42.4095-20.1854 41.8664-20.7659 41.1174-21.1778 40.3684-21.5898 39.3011-21.7958 37.9155-21.7958 36.6047-21.7958 35.3876-21.5149 34.2641-20.9531L34.2641-20.897 34.208-20.9531 31.4554-25.8403 31.5116-25.8965C33.459-27.6567 35.8932-28.5367 38.8143-28.5367 43.3083-28.5367 46.5102-27.3009 48.4201-24.8292Z" fill="#BFBFBF" transform="matrix(1 0 0 1.0116 526.347 301.17)"/><path d="M54.1499 0 54.1499-28.0873 61.6211-28.0873 61.6211 0ZM54.1499-39.3223 61.6211-39.3223 61.6211-32.7498 54.1499-32.7498Z" fill="#BFBFBF" transform="matrix(1 0 0 1.0116 526.347 301.17)"/><path d="M85.4953-10.7294 91.5622-6.74096 91.506-6.68478C90.1952-4.4378 88.4164-2.67766 86.1694-1.40437 83.9599-0.131074 81.5631 0.505572 78.979 0.505572 76.9567 0.505572 75.0655 0.131074 73.3054-0.617921 71.5453-1.40437 70.0098-2.45296 68.6991-3.7637 67.3883-5.07444 66.3397-6.60988 65.5533-8.37002 64.8043-10.1302 64.4298-12.0026 64.4298-13.9875 64.4298-15.9723 64.8043-17.8448 65.5533-19.6049 66.3397-21.3651 67.3883-22.9005 68.6991-24.2113 70.0098-25.522 71.5453-26.5519 73.3054-27.3009 75.0655-28.0873 76.9567-28.4805 78.979-28.4805 81.5631-28.4805 83.9599-27.8439 86.1694-26.5706 88.4164-25.2973 90.1952-23.5372 91.506-21.2902L91.5622-21.234 85.4391-17.2456 85.3829-17.3018C84.7838-18.6874 83.9037-19.8109 82.7427-20.6723 81.6192-21.5336 80.3647-21.9643 78.979-21.9643 77.9679-21.9643 77.0129-21.7396 76.1141-21.2902 75.2528-20.8782 74.5038-20.2978 73.8671-19.5488 73.2305-18.8372 72.7249-17.9946 72.3504-17.0209 71.9759-16.0847 71.7887-15.0735 71.7887-13.9875 71.7887-12.9014 71.9759-11.8903 72.3504-10.9541 72.7249-10.0178 73.2305-9.19392 73.8671-8.48237 74.5038-7.80828 75.2528-7.26525 76.1141-6.85331 77.0129-6.47881 77.9679-6.29156 78.979-6.29156 80.3647-6.29156 81.6192-6.66606 82.7427-7.41505 83.9037-8.2015 84.7838-9.28754 85.3829-10.6732L85.4391-10.7855Z" fill="#BFBFBF" transform="matrix(1 0 0 1.0116 526.347 301.17)"/><path d="M160.084-39.3223 160.084 0 152.613 0 152.669-14.0998C152.669-15.1484 152.444-16.1408 151.995-17.0771 151.583-18.0133 151.021-18.8185 150.309-19.4926 149.635-20.2041 148.811-20.7472 147.838-21.1217 146.901-21.5336 145.89-21.7396 144.804-21.7396 143.718-21.7396 142.707-21.5336 141.771-21.1217 140.835-20.7472 140.011-20.2041 139.299-19.4926 138.625-18.7811 138.082-17.9572 137.67-17.0209 137.258-16.0847 137.052-15.0735 137.052-13.9875 137.052-12.9014 137.258-11.8903 137.67-10.9541 138.082-10.0178 138.625-9.19392 139.299-8.48237 140.011-7.80828 140.835-7.26525 141.771-6.85331 142.707-6.44136 143.718-6.23539 144.804-6.23539 145.329-6.23539 145.853-6.27284 146.377-6.34773 146.901-6.42263 147.388-6.57243 147.838-6.79713L147.894-6.79713 147.95-6.79713 150.703-1.74141 150.646-1.68524C148.811-0.262148 146.564 0.449397 143.905 0.449397 141.883 0.449397 139.992 0.0748995 138.232-0.674096 136.472-1.42309 134.936-2.45296 133.626-3.7637 132.315-5.07444 131.266-6.60988 130.48-8.37002 129.731-10.1302 129.356-12.0026 129.356-13.9875 129.356-15.9723 129.731-17.8448 130.48-19.6049 131.266-21.3651 132.315-22.9005 133.626-24.2113 134.936-25.522 136.472-26.5519 138.232-27.3009 139.992-28.0499 141.883-28.4244 143.905-28.4244 147.725-28.4244 150.646-27.0013 152.669-24.1551L152.669-39.3223Z" fill="#BFBFBF" transform="matrix(1 0 0 1.0116 526.347 301.17)"/></g></g></g></svg> \ No newline at end of file diff --git a/installer/version.txt b/installer/version.txt new file mode 100644 index 0000000000000000000000000000000000000000..56fea8a08d2faa60fec80b40241566d3c39744f7 --- /dev/null +++ b/installer/version.txt @@ -0,0 +1 @@ +3.0.0 \ No newline at end of file diff --git a/libraries b/libraries new file mode 160000 index 0000000000000000000000000000000000000000..fe74dcde23ebf9749dee533e2e4d13d369f6d51a --- /dev/null +++ b/libraries @@ -0,0 +1 @@ +Subproject commit fe74dcde23ebf9749dee533e2e4d13d369f6d51a diff --git a/rce_workflow b/rce_workflow new file mode 160000 index 0000000000000000000000000000000000000000..6e53d4fdac61f832495bdf5c76da36a87c32cf43 --- /dev/null +++ b/rce_workflow @@ -0,0 +1 @@ +Subproject commit 6e53d4fdac61f832495bdf5c76da36a87c32cf43 diff --git a/scripts/lint.py b/scripts/lint.py new file mode 100644 index 0000000000000000000000000000000000000000..b5ebd2cc25102e88d0effd62143752c8cd197542 --- /dev/null +++ b/scripts/lint.py @@ -0,0 +1,320 @@ +#! python +# +# UNICADO - UNIversity Conceptual Aircraft Design and Optimization +# +# Copyright (C) 2025 UNICADO consortium +# +# 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 <https://www.gnu.org/licenses/>. +# +# Description: +# This file is part of UNICADO. +# +"""Lint the given src directory using UNICADO standards.""" +## @file lint.py +# +# @brief Lint the given src directory using UNICADO standards. +# +# @details Invokes cpplint, cppcheck, and (optionally) clang-tidy +# to lint the given source directory. +# +# @author Sebastian Oberschwendtner, sebastian.oberschwendtner@gmail.com + +# === Imports === +import argparse +import errno +import json +import os +import sys +from pathlib import Path +from shutil import which + +# === Meta data === +__version__ = "0.1.0" + +# === Default Configuration === +DEFAULT_SETTINGS = { + "std": "c++20", + "cpplint": { + "enable": ["readability", "naming", "performance"], + "disable": [ + "whitespace", + "build/include_order", + "build/include_subdir", + "build/header_guard", + "runtime/indentation_namespace", + ], + "options": ["--linelength=180"], + }, + "cppcheck": { + "enable": ["warning", "performance", "portability", "information"], + "disable": ["missingInclude"], + "options": ["--inline-suppr"], + }, + "clang-tidy": { + "enable": [ + "bugprone-*", + "clang-analyzer-*", + "cppcoreguidelines-*", + "google-*", + "misc-*", + "modernize-*", + "performance-*", + "portability-*", + "readability-*", + ], + "disable": ["misc-include-cleaner"], + "options": [], + }, +} + + +# === Classes === +class Context: + """Context which defines how linter are invoked.""" + + def __init__(self): + """Initialize the context.""" + self.src = Path("") + self.verbose = False + self.preview = False + self.quiet = False + + def invoke(self, command: str) -> int: + """Invoke the command in the current context. + + Args: + command (str): The command to be called. + + Returns: + int: The return value of the command. + """ + # Split the command into tokens + tokens = command.split(" ") + + # Enable quiet output + if self.quiet: + tokens.insert(1, "--quiet") + + # Check if the command can be executed + if not self.check_command(tokens[0]): + print(f"Command '{tokens[0]}' not available!") + return errno.ENOSYS + + # Join the tokens back together + command = " ".join(tokens) + + # Output the command if verbose + if (self.verbose and not self.quiet) or self.preview: + print(60 * "*" + "\nRunning:", command) + + # Run the command + return 0 if self.preview else os.system(command) + + def check_command(self, command: str) -> bool: + """Check if the command can be executed in the current context. + + Args: + command (str): The command to be checked. + + Returns: + bool: True if the command can be executed, False otherwise. + """ + return which(command) is not None + + +# === Linter Calls === +def lint_cpplint(settings: dict, context: Context) -> int: + """Lint the given source directory using cpplint + + Args: + settings (dict): The settings dictionary of which checks to enable/disable. + context (Context): How the linter should be invoked. + + Returns: + int: The return code of the command. + """ + # Get the list of enabled and disabled checks + filter_list = ",".join("+" + item for item in settings["cpplint"]["enable"]) + filter_list += "," + ",".join("-" + item for item in settings["cpplint"]["disable"]) + + # Assembly the command + cmd = ( + "cpplint " + f"--filter={filter_list} " + "--recursive " + f"{' '.join(settings['cpplint']['options'])} " + f"{context.src.resolve()}" + ) + + # Invoke the linter + return context.invoke(cmd) + + +def lint_cppcheck(settings: dict, context: Context) -> int: + """Lint the given source directory using cppcheck. + + Args: + settings (dict): The settings dictionary of which checks to enable/disable. + context (Context): How the linter should be invoked. + + Returns: + int: The return code of the command. + """ + # Get the list of enabled and disabled checks + enable_list = ",".join(settings["cppcheck"]["enable"]) + disable_list = ",".join(settings["cppcheck"]["disable"]) + + # Assembly the command + cmd = ( + "cppcheck " + f"--disable={disable_list} " + f"--enable={enable_list} " + f"--std={settings['std']} " + "--language=c++ " + "--check-level=exhaustive " + f"{' '.join(settings['cppcheck']['options'])} " + f"{context.src.resolve()}" + ) + + # Invoke the linter + return context.invoke(cmd) + + +def lint_clang_tidy(settings: dict, context: Context) -> int: + """Lint the given source directory using clang-tidy. + + Args: + settings (dict): The settings dictionary of which checks to enable/disable. + context (Context): How the linter should be invoked. + + Returns: + int: The return code of the command. + """ + # Get the list of enabled and disabled checks + filter_list = ",".join(settings["clang-tidy"]["enable"]) + filter_list += "," + ",".join( + "-" + item for item in settings["clang-tidy"]["disable"] + ) + + # Generate a list with all source files + # => Use os.walk to be compatible with python 3.11 + sources = "" + for dirpath, _, filenames in os.walk(context.src): + for filename in filenames: + if filename.endswith(".cpp"): + sources += str(Path(dirpath).resolve() / filename) + " " + + # Assembly the command + cmd = ( + "clang-tidy " + f"--checks={filter_list} " + "--header-filter=.* " + f"-extra-arg=-std={settings['std']} " + f"{' '.join(settings['clang-tidy']['options'])} " + f"{sources}" + ) + + # Invoke the linter + return context.invoke(cmd) + + +def read_configuration_file(file: Path) -> dict: + """Read the configuration file and return the settings dictionary. + + Args: + file (Path): The path to the configuration file. + + Returns: + dict: The settings dictionary. + """ + try: + with open(file, "r", encoding="utf-8") as config: + return json.load(config) + except OSError as e: + print(f"Error reading configuration file: {e}\nUsing default settings!") + return DEFAULT_SETTINGS + + +# === Main === +if __name__ == "__main__": + # Parse the command line arguments + parser = argparse.ArgumentParser( + description="Lint the given src directory using UNICADO standards. " + "All files within the directory will be linted recursively." + ) + parser.add_argument("src", help="source directory to lint", default=".", nargs="?") + parser.add_argument( + "--config", help="use custom configuration file", type=Path, default=None + ) + parser.add_argument( + "--dry-run", action="store_true", help="preview how the linters will be called" + ) + parser.add_argument( + "--dump-config", action="store_true", help="dump the default configuration" + ) + parser.add_argument( + "-e", + "--extra", + action="store_true", + help="enable extra linters (e.g. clang-tidy)", + ) + parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="only output errors and no progress information", + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="enable verbose output" + ) + parser.add_argument( + "--version", action="version", version=f"%(prog)s - Version: {__version__}" + ) + args = parser.parse_args() + + # Read the configuration file if given + settings_ = ( + read_configuration_file(args.config) if args.config else DEFAULT_SETTINGS + ) + + # Check if the source directory exists + if not os.path.exists(args.src): + print(f"The source directory '{args.src}' does not exist") + sys.exit(errno.ENOENT) + + # Dump the default configuration if requested + if args.dump_config: + print(json.dumps(DEFAULT_SETTINGS, indent=4)) + sys.exit(0) + + # Lint the source directory + print(f"Linting source directory '{args.src}'") + + # Setup the context + context_ = Context() + context_.src = Path(args.src) + context_.preview = args.dry_run + context_.quiet = args.quiet + context_.verbose = args.verbose + + # Call the default linters + ret_val = lint_cpplint(settings_, context_) + ret_val += lint_cppcheck(settings_, context_) + + # Call the extra linters + if args.extra: + ret_val += lint_clang_tidy(settings_, context_) + + # Return the return value of the linters + sys.exit(ret_val) diff --git a/utilities b/utilities new file mode 160000 index 0000000000000000000000000000000000000000..f2a3b7406c7f123bafa6c7a36b13a9a17f261a51 --- /dev/null +++ b/utilities @@ -0,0 +1 @@ +Subproject commit f2a3b7406c7f123bafa6c7a36b13a9a17f261a51