From acbb18235cc48ca16a52bef48d763ef65800d413 Mon Sep 17 00:00:00 2001 From: Kristina Mazur <kristina.mazur@tum.de> Date: Fri, 28 Feb 2025 19:45:31 +0100 Subject: [PATCH] Initial open source version --- .gitattributes | 10 + .gitignore | 55 + .gitlab-ci.yml | 0 .gitlab/issue_templates/Default.md | 18 + .gitlab/issue_templates/Todo.md | 16 + .gitlab/issue_templates/bug_report.md | 22 + .../issue_templates/documentation_request.md | 14 + .gitlab/issue_templates/feature_request.md | 17 + .gitlab/issue_templates/testing_request.md | 24 + .gitlab/merge_request_templates/Default.md | 29 + .gitmodules | 21 + CMakeLists.txt | 85 ++ CMakePresets.json | 216 +++ CODEOWNERS | 13 + LICENSE | 674 ++++++++ Pipfile | 21 + Pipfile.lock | 763 +++++++++ README.md | 37 + aircraft_design | 1 + aircraft_references | 1 + cmake/PackageUnicado.cmake | 81 + engines | 1 + installer/CMakeLists.txt | 49 + installer/UNICADOLogo90.svg | 108 ++ installer/UNICADOinstaller.py | 588 +++++++ installer/iconUNICADO.ico | Bin 0 -> 265118 bytes installer/image_rc.py | 1196 +++++++++++++++ installer/own_python_packages.txt | 6 + installer/python_module_list.txt | 5 + installer/standard_python_packages.txt | 10 + installer/sub_functions/abort_install.py | 41 + installer/sub_functions/browse_folder.py | 171 +++ .../change_execution_rights_on_linux.py | 19 + installer/sub_functions/check_tool.py | 179 +++ installer/sub_functions/current_text.py | 24 + installer/sub_functions/delete_tool.py | 55 + installer/sub_functions/install_unicado.py | 1360 +++++++++++++++++ installer/sub_functions/integration_step.py | 196 +++ installer/sub_functions/last_step.py | 134 ++ installer/sub_functions/line_edit.py | 108 ++ installer/sub_functions/next_step.py | 307 ++++ installer/sub_functions/remove_tigl_entry.py | 83 + .../sub_functions/repository_integration.py | 130 ++ installer/sub_functions/resource_path.py | 29 + installer/sub_functions/retranslate_ui.py | 75 + installer/sub_functions/uninstall_steps.py | 249 +++ .../update_additional_software.py | 119 ++ .../sub_functions/update_design_tools.py | 79 + .../sub_functions/update_projects_folder.py | 54 + installer/sub_functions/update_steps.py | 431 ++++++ installer/sub_functions/write_json_file.py | 104 ++ .../write_path_to_environment.py | 88 ++ installer/unicadoICON.svg | 1 + installer/version.txt | 1 + libraries | 1 + rce_workflow | 1 + scripts/lint.py | 320 ++++ utilities | 1 + 58 files changed, 8441 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .gitlab/issue_templates/Default.md create mode 100644 .gitlab/issue_templates/Todo.md create mode 100644 .gitlab/issue_templates/bug_report.md create mode 100644 .gitlab/issue_templates/documentation_request.md create mode 100644 .gitlab/issue_templates/feature_request.md create mode 100644 .gitlab/issue_templates/testing_request.md create mode 100644 .gitlab/merge_request_templates/Default.md create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 CODEOWNERS create mode 100644 LICENSE create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 README.md create mode 160000 aircraft_design create mode 160000 aircraft_references create mode 100644 cmake/PackageUnicado.cmake create mode 160000 engines create mode 100644 installer/CMakeLists.txt create mode 100644 installer/UNICADOLogo90.svg create mode 100644 installer/UNICADOinstaller.py create mode 100644 installer/iconUNICADO.ico create mode 100644 installer/image_rc.py create mode 100644 installer/own_python_packages.txt create mode 100644 installer/python_module_list.txt create mode 100644 installer/standard_python_packages.txt create mode 100644 installer/sub_functions/abort_install.py create mode 100644 installer/sub_functions/browse_folder.py create mode 100644 installer/sub_functions/change_execution_rights_on_linux.py create mode 100644 installer/sub_functions/check_tool.py create mode 100644 installer/sub_functions/current_text.py create mode 100644 installer/sub_functions/delete_tool.py create mode 100644 installer/sub_functions/install_unicado.py create mode 100644 installer/sub_functions/integration_step.py create mode 100644 installer/sub_functions/last_step.py create mode 100644 installer/sub_functions/line_edit.py create mode 100644 installer/sub_functions/next_step.py create mode 100644 installer/sub_functions/remove_tigl_entry.py create mode 100644 installer/sub_functions/repository_integration.py create mode 100644 installer/sub_functions/resource_path.py create mode 100644 installer/sub_functions/retranslate_ui.py create mode 100644 installer/sub_functions/uninstall_steps.py create mode 100644 installer/sub_functions/update_additional_software.py create mode 100644 installer/sub_functions/update_design_tools.py create mode 100644 installer/sub_functions/update_projects_folder.py create mode 100644 installer/sub_functions/update_steps.py create mode 100644 installer/sub_functions/write_json_file.py create mode 100644 installer/sub_functions/write_path_to_environment.py create mode 100644 installer/unicadoICON.svg create mode 100644 installer/version.txt create mode 160000 libraries create mode 160000 rce_workflow create mode 100644 scripts/lint.py create mode 160000 utilities diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5de13d2 --- /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 0000000..59bd72d --- /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 0000000..e69de29 diff --git a/.gitlab/issue_templates/Default.md b/.gitlab/issue_templates/Default.md new file mode 100644 index 0000000..ed6f8d8 --- /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 0000000..07bdd0f --- /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 0000000..b906703 --- /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 0000000..bc94cf6 --- /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 0000000..2c12fdb --- /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 0000000..0289f4d --- /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 0000000..00be17d --- /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 0000000..6b220a1 --- /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 0000000..dd6e951 --- /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 0000000..0396d3d --- /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 0000000..bae5ee3 --- /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 0000000..e06ae69 --- /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 0000000..845615c --- /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 0000000..caf89f4 --- /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 0000000..505df0f --- /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 0000000..f1c70da --- /dev/null +++ b/aircraft_design @@ -0,0 +1 @@ +Subproject commit f1c70dad7cffec6776c26d4521c328f824f6fe21 diff --git a/aircraft_references b/aircraft_references new file mode 160000 index 0000000..23227da --- /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 0000000..d0a7201 --- /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 0000000..922a8f1 --- /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 0000000..72a46a6 --- /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 0000000..90767cb --- /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,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"/> + </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,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"/> + </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 0000000..29aa0e4 --- /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 GIT binary patch literal 265118 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Ψ=*JZm9ZE<iUj#syrkwf^|b<6puV8_!v;+*-`XE<)Lv7az8st0s@JEx+xIr zKs;1=Nbte5faKW_O~C)4@&g<INamx;gJr?B3&;ix{h;6h#Q;bWQe7~x!6FMJ_J@H1 z+(iSiq4Ioa@}N8m3sR8zd}!sr00W|@4+;$w`40>XpjI$UKSUmuo4^)=TiLKE$B;h& z>0#oOZ(soD515G%{V>(w;u$0aZEk|ALk2$N`VZR8Le&p*El58&d&7MU%HMp*ISA55 zN0FC<$HxEv|DhdeX!ZnWRup+i+X5cqAoJxI7%CveEhthT@e3=mK&crd4@)nQ0uP=% z!CW~81`D_s!M;ZnX;A$Zkd`J!@`C9Hg(=udka~~{KrR7cP;!!EfP^_n0W|f(0}Lt; zF7p`JA+<SD@&HxGpzwhND#&06Mno6`14tfR=0c>vBrJMB)iGEeYA;xl0oI!UIj{k& zA6(|cB;maYBzaIeMv{j`4>-HYF)%>tB!mKxS3$bL?KM6I21uQTkcZ~Z|1fz-or;i$ z=mn7v7#Ki-0}3aEG*W>Al?T`Pa0T$-1t(xo;Ms%nG+Z7Le4r2q%cFP@k(xjW5F{@T zO&1U&5Z(tR0I)nLAwnb(g&)ZMe;@-F{IG-pR)g?9D1t%y`N546uq0?a45Sl+|1mIt z+92$ZHZep2lJh|k43cMt<_oX_MC^kTEGTg>gFFhBM2TIvJR``XFnL7mfg=c{p8@1h zm^{Q~Q1Sr-L>}Qlq|gV~px|+jdPt`PWCV)y{(pd!DE0^kAcZcdJpz-5CjqF%Q0fmv z9yaO$>q$dpL7fnA;^v2T#Nah6Tmb`k+=U(5Rzfa|AW9m*14kf7gEJ9Q+JML(0Lz2o z1RTc@K_v14SRNcGkPao19EkaW0i3o#Osx0^L>`)E{-A~qsKJFI{{ut*9|L&U1RANB z^8X<NUr>D?pfr+G8o&c%P=8~QKLC~o1rVryg4%<m{{wi~1r$J_<b)~z11t{?p#T35 zV9G<29wY!7AQY1EpeP0<D~LvTL64*Yl5)Tv`-e>)lJ~$K_=8lmAWQ%m52+!3V3mgy zE+F+E@W_K42<mZy)S+T<!UETcpdKx%9Eb;zX9oEc8jK)O1cnw~pb-Il^8EiFz$<oy zW^f(_`w-Og0fz#LB$7OO*#T3BRQN%Y2}}k;gEI!i3UDkyL=hw*c}N6;4FClqLL-a? zmI4!m<gp|la3KiRjA(bl42IH>2mq;p3ZPNY2tX~mk<@^5I7mA*l_N=knQ(cm(FbpI zV-#*+&EPTsWH?I24wC}Y@BqXbJ)mYAR6j0xND9X+Ji(@evNcpc_TUFs!(bKIgCF5R zn0e?lG=<;@K2XL08;>LS;4zFfb%X7K#4s-J!#sdB^@8<7vH&jc!yE`!ik$$3Fi!7* f3Rc{)3pNns0Nl=lL@}<=2OIDKd)WY%8<7M6W&r8t literal 0 HcmV?d00001 diff --git a/installer/image_rc.py b/installer/image_rc.py new file mode 100644 index 0000000..2824d89 --- /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 0000000..4341169 --- /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 0000000..f2a0771 --- /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 0000000..2478e03 --- /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 0000000..72ddbc1 --- /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 0000000..04e5eef --- /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 0000000..a17d28f --- /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 0000000..cc4c87d --- /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 0000000..58ef13c --- /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 0000000..ffeed5b --- /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 0000000..00357a5 --- /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 0000000..4ae865a --- /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 0000000..3945a8e --- /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 0000000..e3abbdd --- /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 0000000..edfe8dd --- /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 0000000..ce51182 --- /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 0000000..22c2669 --- /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 0000000..35e8274 --- /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 0000000..78b50c0 --- /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 0000000..4641c2f --- /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 0000000..c306fe2 --- /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 0000000..6453ef8 --- /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 0000000..59eece3 --- /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 0000000..156de38 --- /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 0000000..c8ce4fe --- /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 0000000..015e88b --- /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 0000000..08022fe --- /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 0000000..56fea8a --- /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 0000000..fe74dcd --- /dev/null +++ b/libraries @@ -0,0 +1 @@ +Subproject commit fe74dcde23ebf9749dee533e2e4d13d369f6d51a diff --git a/rce_workflow b/rce_workflow new file mode 160000 index 0000000..6e53d4f --- /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 0000000..b5ebd2c --- /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 0000000..f2a3b74 --- /dev/null +++ b/utilities @@ -0,0 +1 @@ +Subproject commit f2a3b7406c7f123bafa6c7a36b13a9a17f261a51 -- GitLab