diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 84b744b081ec1cd11906afd584b7f5418bbd689d..0000000000000000000000000000000000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,43 +0,0 @@ -environment: - - PYTHON_ARCH: "64" - PYTHON: "C:\\Miniconda38-x64" - - matrix: - - PYTHON_VERSION: "3.8" - -install: - # windows config (for installation) - - cmd: "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - cmd: setlocal - - cmd: set ANACONDA_API_TOKEN= - # conda config - - conda config --set always_yes yes --set changeps1 no - - conda update -q conda - - conda install conda-build anaconda-client - - pip install -i https://pypi.anaconda.org/psyplot/simple --no-deps psyplot-ci-orb - - conda config --add channels conda-forge - - conda config --add channels psyplot - - conda info -a - - conda list - # windows config - - cmd: endlocal - - cmd: 'SET PYTHONWARNINGS=ignore:mode:DeprecationWarning:docutils.io:245' - - cmd: "IF NOT DEFINED APPVEYOR_REPO_TAG_NAME (SET GIT_BRANCH=%APPVEYOR_REPO_BRANCH%)" - - cmd: "IF NOT DEFINED APPVEYOR_REPO_TAG_NAME (conda config --add channels psyplot/label/%APPVEYOR_REPO_BRANCH%)" - -build: off - -test_script: - - cmd: setlocal - - cmd: set ANACONDA_API_TOKEN= - - cmd: conda build ci/conda-recipe --python %PYTHON_VERSION% - - cmd: endlocal - -deploy_script: - - cmd: " - IF NOT DEFINED APPVEYOR_REPO_TAG_NAME ( - deploy-conda-recipe -l %APPVEYOR_REPO_BRANCH% -py %PYTHON_VERSION% ci/conda-recipe - ) ELSE ( - deploy-conda-recipe -py %PYTHON_VERSION% ci/conda-recipe - )" diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index c050d3dcbc45d3de2fde580082ad7dabcdb13be7..0000000000000000000000000000000000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,113 +0,0 @@ -version: 2.1 - -orbs: - psyplot: psyplot/psyplot-ci-orb@1.5.25 - mattermost-plugin-notify: nathanaelhoun/mattermost-plugin-notify@1.2.0 - -executors: - default: psyplot/default - macos: psyplot/macos - -parameters: - unit-test-executor: - description: Executor for the unit tests. Can be default or macos - type: string - default: default - deploy-release: - description: Deploy the comment as a new release to github and pypi - type: boolean - default: false - run-tests: - description: Run the test suite - type: boolean - default: true - build_docs: - description: Build the documentation - type: boolean - default: true - -workflows: - build-and-test: - unless: << pipeline.parameters.deploy-release >> - jobs: - - psyplot/install-and-build: - name: install - exec_environment: << pipeline.parameters.unit-test-executor >> - setup_env: true - build_args: "--no-test" - build_docs: << pipeline.parameters.build_docs >> - - psyplot/test-parallel: - name: run-tests - parallelism: 1 - pytest_args: --cov=psyplot_gui - run-job: << pipeline.parameters.run-tests >> - requires: - - install - - psyplot/build-docs: - name: test-docs - run-job: << pipeline.parameters.build_docs >> - builders: linkcheck - requires: - - install - - mattermost-plugin-notify/approval-notification: - name: notify-deploy - context: mattermost - message: >- - Hello @all! A workflow on https://app.circleci.com/pipelines/github/psyplot/psyplot-gui is awaiting your approval. - Please check the uploaded docs and builds prior to approval. - requires: - - run-tests - - test-docs - - hold-for-deploy: - type: approval - requires: - - notify-deploy - - psyplot/deploy-pkg: - exec_environment: << pipeline.parameters.unit-test-executor >> - context: anaconda - requires: - - hold-for-deploy - - psyplot/deploy-docs: - fingerprint: "19:54:3e:9e:7b:73:1c:45:ee:4c:7b:73:8a:46:96:3a" - run-job: << pipeline.parameters.build_docs >> - requires: - - hold-for-deploy - filters: - branches: - only: master - - psyplot/trigger-release-workflow: - context: trigger-release - filters: - branches: - only: master - requires: - - psyplot/deploy-pkg - - psyplot/deploy-docs - publish-release: - when: << pipeline.parameters.deploy-release >> - jobs: - - psyplot/create-tag: - ssh-fingerprints: "19:54:3e:9e:7b:73:1c:45:ee:4c:7b:73:8a:46:96:3a" - context: psyplot-admin - user-name: psyplot-admin - publish-release: true - publish-version-tag: true - - mattermost-plugin-notify/approval-notification: - name: notify-release - context: mattermost - message: >- - Hello @all! A new release has been created at https://github.com/psyplot/psyplot-gui/releases. - Please review it carefully, publish it and approve the upload to pypi. - requires: - - psyplot/create-tag - - hold-for-pypi: - type: approval - requires: - - notify-release - - psyplot/deploy-pypi: - context: pypi - requires: - - hold-for-pypi - filters: - branches: - only: master diff --git a/.cruft.json b/.cruft.json new file mode 100644 index 0000000000000000000000000000000000000000..655105206ed1d77a2fd556a4ee2eb3e058cdbd14 --- /dev/null +++ b/.cruft.json @@ -0,0 +1,44 @@ +{ + "template": "https://codebase.helmholtz.cloud/psyplot/psyplot-plugin-template.git", + "commit": "f0ad88b667f92604f1c0383a044c4d2546d30887", + "checkout": null, + "context": { + "cookiecutter": { + "project_authors": "Philipp S. Sommer", + "project_author_emails": "philipp.sommer@hereon.de", + "project_maintainers": "Philipp S. Sommer", + "project_maintainer_emails": "philipp.sommer@hereon.de", + "gitlab_host": "codebase.helmholtz.cloud", + "gitlab_username": "psyplot", + "git_remote_protocoll": "ssh", + "institution": "Helmholtz-Zentrum Hereon", + "institution_url": "https://www.hereon.de", + "copyright_holder": "Helmholtz-Zentrum hereon GmbH", + "copyright_year": "2021-2024", + "use_reuse": "yes", + "code_license": "LGPL-3.0-only", + "documentation_license": "CC-BY-4.0", + "supplementary_files_license": "CC0-1.0", + "project_title": "psyplot-gui", + "project_slug": "psyplot-gui", + "package_folder": "psyplot_gui", + "project_short_description": "Graphical user interface for the psyplot package", + "keywords": "visualization,psyplot,netcdf,raster,cartopy,earth-sciences,pyqt,qt,ipython,jupyter,qtconsole", + "documentation_url": "https://psyplot.github.io/psyplot-gui", + "use_markdown_for_documentation": "no", + "ci_matrix": "pipenv", + "requires_gui": "yes", + "deploy_package_in_ci": "yes", + "deploy_pages_in_ci": "git-push", + "_extensions": [ + "local_extensions.UnderlinedExtension" + ], + "_template": "https://codebase.helmholtz.cloud/psyplot/psyplot-plugin-template.git" + } + }, + "directory": null, + "skip": [ + ".git", + ".mypy_cache" + ] +} diff --git a/.cruft.json.license b/.cruft.json.license new file mode 100644 index 0000000000000000000000000000000000000000..919c9c1ed96213967f38fbe568505a4d787c5ae1 --- /dev/null +++ b/.cruft.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC0-1.0 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000000000000000000000000000000000..18607fff260a0edb1b087f0e61ecbe1915fa0544 --- /dev/null +++ b/.flake8 @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +[flake8] +extend-ignore = + E203 + E402 + E501 + W503 diff --git a/.gitattributes b/.gitattributes index f611cdb347f47afdf2e7ad36368d25e47e0ed9d7..9e178d0fe062b27bb96002cd3765e4c0d4279b63 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,5 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + psyplot_gui/_version.py export-subst diff --git a/.github/ISSUE_TEMPLATE/change_feature.md b/.github/ISSUE_TEMPLATE/change_feature.md deleted file mode 100644 index 436208c22bf97bbc621939a9b7ab2faeb2078bcf..0000000000000000000000000000000000000000 --- a/.github/ISSUE_TEMPLATE/change_feature.md +++ /dev/null @@ -1,17 +0,0 @@ -#### Summary -[Describe the requested change in one or two lines. -This should also be mentioned in the title of this issue.] - -#### Reason -[Why do you think, this is useful?] - -#### Current behaviour -[How is the current behaviour/framework?] - -#### New behaviour -[Provide here some more explanation that goes beyond the summary above -(or delete this paragraph, if everything is explained above), -and describe the changes you would like to see] - -#### Examples -[images, code-snippets or URLs to other repositories] diff --git a/.github/ISSUE_TEMPLATE/new_feature.md b/.github/ISSUE_TEMPLATE/new_feature.md deleted file mode 100644 index cdb2e7964c9741756673c3c0680ebd1b8d559735..0000000000000000000000000000000000000000 --- a/.github/ISSUE_TEMPLATE/new_feature.md +++ /dev/null @@ -1,13 +0,0 @@ -#### Summary -[Describe the new requested feature in one or two lines. -This should also be mentioned in the title of this issue.] - -#### Reason -[Why do you think, this is useful?] - -#### Detailed explanation -[Provide here some more explanation that goes beyond the summary above -(or delete this paragraph, if everything is explained above)] - -#### Examples -[images, code-snippets or URLs to other repositories] diff --git a/.github/issue_template.md b/.github/issue_template.md deleted file mode 100644 index 142a180afc8dc68d6c8e246713868a9f9acffb05..0000000000000000000000000000000000000000 --- a/.github/issue_template.md +++ /dev/null @@ -1,30 +0,0 @@ -#### Code Sample, a copy-pastable example if possible - -```python -# Your code here - -``` -#### Problem description - -[this should explain **why** the current behavior is a problem and why the expected output is a better solution.] - -#### Expected Output -What should have been expected? You can hide large error messages within ``<details></details>`` tags, e.g. - -<details> -very long error message -</details> - -#### Output of ``psyplot -aV`` - -<details> -# Paste the output of the command ``psyplot -aV`` (ran from the command line) - -</details> - -#### NOTE -This is a bug report. - -For requesting new features, use [this template](https://github.com/psyplot/psyplot-gui/issues/new?template=new_feature.md&title=NEW+FEATURE:). - -For changing existing features, use [this template](https://github.com/psyplot/psyplot-gui/issues/new?template=change_feature.md&title=CHANGE+FEATURE:). diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 4eb59a1c3880d06b384092ada69932803ca99d2e..0000000000000000000000000000000000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,4 +0,0 @@ - - [ ] Closes #xxxx (remove if there is no corresponding issue, which should only be the case for minor changes) - - [ ] Tests added (for all bug fixes or enhancements) - - [ ] Tests passed (for all non-documentation changes) - - [ ] Fully documented, including `CHANGELOG.rst` for all changes diff --git a/.gitignore b/.gitignore index 875a53c6a6bed6bea610571a160dfde33c80a6d4..0407d0f41e0be1d6a20660d8d5a2b81b7e7aae10 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,17 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] +*$py.class # C extensions *.so -# mypy cache -.mypy_cache - # Distribution / packaging .Python -env/ build/ develop-eggs/ dist/ @@ -22,9 +23,12 @@ lib64/ parts/ sdist/ var/ +wheels/ +share/python-wheels/ *.egg-info/ .installed.cfg *.egg +MANIFEST # PyInstaller # Usually these files are written by a python script from a template @@ -39,13 +43,17 @@ pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ +.nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml -*,cover +*.cover +*.py,cover +.hypothesis/ .pytest_cache/ +cover/ # Translations *.mo @@ -53,27 +61,98 @@ coverage.xml # Django stuff: *.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy # Sphinx documentation -docs/api/ docs/_build/ -docs/index.doctree # PyBuilder +.pybuilder/ target/ -# Spyder project +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings .spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ -# Example ipython notebook checkpoints -*.ipynb_checkpoints/ +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +static/ + +docs/api +psyplot_gui/migrations/00*.py +docs/_static/orcid.* + +# ignore Pipfile.lock files in ci +# if a lock-file needs to be added, add it with `git add -f` +ci/matrix/*/Pipfile.lock # rc files *matplotlibrc *psyplotguirc.yml *psyplotrc.yml *debug_psyplot.log* - -# conda build files -ci/conda_recipe/psyplot-gui/meta.yaml -ci/conda-recipe/recipe_append.yaml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..02d80bdbf9f99de6b71cc06ca4250c6ce3abd364 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +image: python:3.9 + +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + +cache: + paths: + - .cache/pip + +before_script: + # replace git internal paths in order to use the CI_JOB_TOKEN + - apt-get update -y && apt-get install -y pandoc graphviz + - python -m pip install -U pip + +test-package: + stage: test + script: + - pip install build twine + - make dist + - twine check dist/* + artifacts: + name: python-artifacts + paths: + - "dist/*" + expire_in: 7 days + +test: + stage: test + variables: + PIPENV_PIPFILE: "ci/matrix/${SCENARIO}/Pipfile" + # disable sandboxing, otherwise chrome reports errors when the + # container runs as root + # https://doc.qt.io/qt-5/qtwebengine-platform-notes.html#sandboxing-support + QTWEBENGINE_DISABLE_SANDBOX: "true" + script: + # install necessary libraries for pyqt + - apt-get install -y xvfb python3-pyqt5.qtwebengine + - pip install pipenv + - pipenv install + - xvfb-run make pipenv-test + parallel: + matrix: + - SCENARIO: + - default + artifacts: + name: pipfile + paths: + - "ci/matrix/${SCENARIO}/*" + expire_in: 30 days + coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' + +test-docs: + stage: test + variables: + # disable sandboxing, otherwise chrome reports errors when the + # container runs as root + # https://doc.qt.io/qt-5/qtwebengine-platform-notes.html#sandboxing-support + QTWEBENGINE_DISABLE_SANDBOX: "true" + script: + # install necessary libraries for pyqt + - apt-get install -y xvfb python3-pyqt5.qtwebengine + - make dev-install + # install PyQt5 (not part of requirements.txt because this is complicated + # to install on different platforms) + - pip install PyQt5 PyQtWebEngine + - xvfb-run make -C docs html + - xvfb-run make -C docs linkcheck + artifacts: + paths: + - docs/_build + + +deploy-package: + stage: deploy + needs: + - test-package + - test-docs + - test + only: + - master + script: + - pip install twine + - TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/* + + + +deploy-docs: + stage: deploy + only: + - master + needs: + - test-docs + image: node:21 + before_script: + - npm install -g gh-pages@6.1.1 + - mkdir .gh-pages-cache + script: + # make sure, the DEPLOY_TOKEN is defined + - >- + [ ${CI_DEPLOY_TOKEN} ] || + echo "The CI_DEPLOY_TOKEN variable is not set. Please create an access + token with scope 'read_repository' and 'write_repository'" && + [ ${CI_DEPLOY_TOKEN} ] + - >- + CACHE_DIR=$(realpath .gh-pages-cache) + gh-pages + --dotfiles + --nojekyll + --branch gh-pages + --repo https://ci-user:${CI_DEPLOY_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git + --user "${CI_COMMIT_AUTHOR}" + --message "CI Pipeline ${CI_PIPELINE_ID}, commit ${CI_COMMIT_SHORT_SHA}" + --dist docs/_build/html diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..48de8fe21cfe9c1cca28129993605708cc19856f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +# https://pre-commit.com/ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + # isort should run before black as black sometimes tweaks the isort output + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + args: + - --profile + - black + - --line-length + - "79" + - --filter-files + - -skip-gitignore + - --float-to-top + # https://github.com/python/black#version-control-integration + - repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black + args: + - --line-length + - "79" + - --exclude + - venv + # - repo: https://github.com/keewis/blackdoc + # rev: v0.3.8 + # hooks: + # - id: blackdoc + - repo: https://github.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v1.0.1 + # hooks: + # - id: mypy + # additional_dependencies: + # - types-PyYAML + # args: + # - --ignore-missing-imports + + - repo: https://github.com/fsfe/reuse-tool + rev: v1.1.2 + hooks: + - id: reuse + + - repo: https://github.com/citation-file-format/cff-converter-python + # there is no release with this hook yet + rev: "44e8fc9" + hooks: + - id: validate-cff diff --git a/.reuse/add_license.py b/.reuse/add_license.py new file mode 100644 index 0000000000000000000000000000000000000000..33428706335e785f35ba3c061f2da3536ad15b57 --- /dev/null +++ b/.reuse/add_license.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + +"""Helper script to add licenses to files. + +This script can be used to apply the licenses and default copyright holders +to files in the repository. + +It uses the short cuts from the ``.reuse/shortcuts.yaml`` file and +adds them to the call of ``reuse annotate``. Any command line option however +overwrites the config in ``shortcuts.yaml`` + +Usage:: + + python .reuse/add_license.py <shortcut> <path> [OPTIONS] +""" + +import os.path as osp +from argparse import ArgumentParser +from textwrap import dedent +from typing import Dict, Optional, TypedDict + +import yaml +from reuse.project import Project +from reuse.vcs import find_root + +try: + from reuse._annotate import add_arguments as _orig_add_arguments + from reuse._annotate import run +except ImportError: + # reuse < 3.0 + from reuse.header import add_arguments as _orig_add_arguments + from reuse.header import run + + +class LicenseShortCut(TypedDict): + """Shortcut to add a copyright statement""" + + #: The copyright statement + copyright: str + + #: year of copyright statement + year: str + + #: SPDX Identifier of the license + license: Optional[str] + + +def load_shortcuts() -> Dict[str, LicenseShortCut]: + """Load the ``shortcuts.yaml`` file.""" + + with open(osp.join(osp.dirname(__file__), "shortcuts.yaml")) as f: + return yaml.safe_load(f) + + +def add_arguments( + parser: ArgumentParser, shortcuts: Dict[str, LicenseShortCut] +): + parser.add_argument( + "shortcut", + choices=[key for key in shortcuts if not key.startswith(".")], + help=( + "What license should be applied? Shortcuts are loaded from " + ".reuse/shortcuts.yaml. Possible shortcuts are %(choices)s" + ), + ) + + _orig_add_arguments(parser) + + parser.set_defaults(func=run) + parser.set_defaults(parser=parser) + + +def main(argv=None): + shortcuts = load_shortcuts() + + parser = ArgumentParser( + prog=".reuse/add_license.py", + description=dedent( + """ + Add copyright and licensing into the header of files with shortcuts + + This script uses the ``reuse annotate`` command to add copyright + and licensing information into the header the specified files. + + It accepts the same arguments as ``reuse annotate``, plus an + additional required `shortcuts` argument. The given `shortcut` + comes from the file at ``.reuse/shortcuts.yaml`` to fill in + copyright, year and license identifier. + + For further information, please type ``reuse annotate --help``""" + ), + ) + add_arguments(parser, shortcuts) + + args = parser.parse_args(argv) + + shortcut = shortcuts[args.shortcut] + + if args.year is None: + args.year = [] + if args.copyright is None: + args.copyright = [] + + if args.license is None and shortcut.get("license"): + args.license = [shortcut["license"]] + elif args.license and shortcut.get("license"): + args.license.append(shortcut["license"]) + args.year.append(shortcut["year"]) + args.copyright.append(shortcut["copyright"]) + + project = Project(find_root()) + args.func(args, project) + + +if __name__ == "__main__": + main() diff --git a/.reuse/shortcuts.yaml b/.reuse/shortcuts.yaml new file mode 100644 index 0000000000000000000000000000000000000000..43a4548ad0d321e79f9e820236914408032c512b --- /dev/null +++ b/.reuse/shortcuts.yaml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +.defaults: &defaults + year: "2021-2024" + copyright: "Helmholtz-Zentrum hereon GmbH" + +# The following dictionaries items map to dictionaries with three possible +# keys: +# +# copyright: The copyright statement +# year: year of copyright statement +# license: SPDX Identifier +docs: + <<: *defaults + license: "CC-BY-4.0" +code: + <<: *defaults + license: "LGPL-3.0-only" +supp: + <<: *defaults + license: "CC0-1.0" diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..5010f375e88e42662217e349baa514226304d26d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "ms-python.python", + "gitlab.featureFlags": {"securityScansFlag": false}, + "python.formatting.provider": "black", + "python.formatting.blackArgs": [ + "--line-length", + "79" + ], + "python.linting.mypyCategorySeverity.note": "Hint", + "python.linting.mypyEnabled": true, + +} diff --git a/.vscode/settings.json.license b/.vscode/settings.json.license new file mode 100644 index 0000000000000000000000000000000000000000..919c9c1ed96213967f38fbe568505a4d787c5ae1 --- /dev/null +++ b/.vscode/settings.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC0-1.0 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f1f066c92e50ab4f1f46e8ac7f0f03fdfad5590d..925c13c835abb688c318102ea988086ca903ad3f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,7 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + v1.4.0 ====== Compatibility fixes and LGPL license diff --git a/CITATION.cff b/CITATION.cff index 734fdd9dbf571cf6002cf646559ed6b01f464074..aa17bfeae864ad963254a5f8be17dac5d1d3e7e1 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + # YAML 1.2 --- cff-version: "1.2.0" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 5c34d708910e70b8b458560e1039237b36180284..0000000000000000000000000000000000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,5 +0,0 @@ -# Contributing to psyplot-gui - -:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: - -For some guidelines, please see the [contribution guidelines for psyplot](https://github.com/psyplot/psyplot/blob/master/CONTRIBUTING.md). diff --git a/COPYING b/COPYING deleted file mode 100644 index f288702d2fa16d3cdf0035b15a9fcbc552cd88e7..0000000000000000000000000000000000000000 --- a/COPYING +++ /dev/null @@ -1,674 +0,0 @@ - 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) <year> <name of author> - - 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) <year> <name of author> - 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/COPYING.LESSER b/COPYING.LESSER deleted file mode 100644 index 0a041280bd00a9d068f503b8ee7ce35214bd24a1..0000000000000000000000000000000000000000 --- a/COPYING.LESSER +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER 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. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser 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 -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt new file mode 100644 index 0000000000000000000000000000000000000000..13ca539f377dc705af32b8d2ce89262298ea2f06 --- /dev/null +++ b/LICENSES/CC-BY-4.0.txt @@ -0,0 +1,156 @@ +Creative Commons Attribution 4.0 International + + Creative Commons Corporation (“Creative Commonsâ€) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is†basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. + +Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +Section 1 – Definitions. + + a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + + d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + + g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. + + i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part; and + + B. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + + 3. Term. The term of this Public License is specified in Section 6(a). + + 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + + 5. Downstream recipients. + + A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + + 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. Other rights. + + 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this Public License. + + 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + + 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; + + b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + + a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. + + b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. + + c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + + a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + + c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + + d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + + e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +Section 8 – Interpretation. + + a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + + c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + + d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.†Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons†or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/LICENSES/CC0-1.0.txt b/LICENSES/CC0-1.0.txt new file mode 100644 index 0000000000000000000000000000000000000000..0e259d42c996742e9e3cba14c677129b2c1b6311 --- /dev/null +++ b/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/LICENSES/LGPL-3.0-only.txt b/LICENSES/LGPL-3.0-only.txt new file mode 100644 index 0000000000000000000000000000000000000000..513d1c01fe5b5b9724e999d8eb041ff17d444eaa --- /dev/null +++ b/LICENSES/LGPL-3.0-only.txt @@ -0,0 +1,304 @@ +GNU LESSER GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. + +0. Additional Definitions. + +As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. + +"The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. + +An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. + +A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". + +The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. + +The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. + +1. Exception to Section 3 of the GNU GPL. +You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. + +2. Conveying Modified Versions. +If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: + + a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. + +3. Object Code Incorporating Material from Library Header Files. +The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license document. + +4. Combined Works. +You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: + + a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license document. + + c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. + + e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) + +5. Combined Libraries. +You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. + +6. Revised Versions of the GNU Lesser General Public License. +The Free Software Foundation may publish revised and/or new versions of the GNU Lesser 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 Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. + +If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. + +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright © 2007 Free Software Foundation, Inc. <http://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) <year> <name of author> + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. + +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) <year> <name of author> + 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 <http://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 <http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/MANIFEST.in b/MANIFEST.in index 3fb481eec6cf05bcd385057c273f061e861a312f..e456215eddb10c7f2701f7d0113c551e181eeda2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,14 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + include README.rst -include LICENSE +include LICENSES/* include psyplot_gui/sphinx_supp/_static/* include psyplot_gui/sphinx_supp/conf.py include psyplot_gui/sphinx_supp/psyplot.rst include psyplot_gui/icons/*.png +include psyplot_gui/icons/*.png.license include psyplot_gui/icons/*.svg -include versioneer.py +include psyplot_gui/icons/*.svg.license include psyplot_gui/_version.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..ebecc994c696d84e0349429bb8ae3263c07c0029 --- /dev/null +++ b/Makefile @@ -0,0 +1,124 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +.PHONY: clean clean-build clean-pyc clean-test coverage dist docs help install lint lint/flake8 lint/black +.DEFAULT_GOAL := help + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean-build clean-pyc clean-test clean-venv ## remove all build, virtual environments, test, coverage and Python artifacts + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + rm -fr .pytest_cache + +clean-venv: # remove the virtual environment + rm -rf venv + +lint/isort: ## check style with flake8 + isort --check psyplot_gui tests +lint/flake8: ## check style with flake8 + flake8 psyplot_gui tests +lint/black: ## check style with black + black --check psyplot_gui tests +lint/reuse: ## check licenses + reuse lint + +lint: lint/isort lint/black lint/flake8 lint/reuse ## check style + +formatting: + isort psyplot_gui tests + black psyplot_gui tests + blackdoc psyplot_gui tests + +quick-test: ## run tests quickly with the default Python + python -m pytest + +pipenv-test: ## run tox + pipenv run isort --check psyplot_gui + pipenv run black --line-length 79 --check psyplot_gui + pipenv run flake8 psyplot_gui + pipenv run pytest -v --cov=psyplot_gui -x + pipenv run reuse lint + pipenv run cffconvert --validate + +test: ## run tox + tox + +test-all: test test-docs ## run tests and test the docs + +coverage: ## check code coverage quickly with the default Python + python -m pytest --cov psyplot_gui --cov-report=html + $(BROWSER) htmlcov/index.html + +docs: ## generate Sphinx HTML documentation, including API docs + $(MAKE) -C docs clean + $(MAKE) -C docs html + $(BROWSER) docs/_build/html/index.html + +test-docs: ## generate Sphinx HTML documentation, including API docs + $(MAKE) -C docs clean + $(MAKE) -C docs linkcheck + +servedocs: docs ## compile the docs watching for changes + watchmedo shell-command -p '*.rst' -c '$(MAKE) -C docs html' -R -D . + +release: dist ## package and upload a release + twine upload dist/* + +dist: clean ## builds source and wheel package + python -m build + ls -l dist + +install: clean ## install the package to the active Python's site-packages + python -m pip install . + +dev-install: clean + python -m pip install -r docs/requirements.txt + python -m pip install -e .[dev] + pre-commit install + +venv-install: clean + python -m venv venv + venv/bin/python -m pip install -r docs/requirements.txt + venv/bin/python -m pip install -e .[dev] + venv/bin/pre-commit install diff --git a/README.rst b/README.rst index 9ad5ad5c208124daf1951598a1b8bf25a14393fb..06db885181f94ca8849cd62be76f7f3814cf47e3 100644 --- a/README.rst +++ b/README.rst @@ -1,60 +1,20 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + Graphical User Interface for the psyplot package ================================================ .. start-badges -.. list-table:: - :stub-columns: 1 - :widths: 10 90 - - * - docs - - |docs| - * - tests - - |circleci| |appveyor| |codecov| - * - package - - |version| |conda| |github| |zenodo| - * - implementations - - |supported-versions| |supported-implementations| - -.. |docs| image:: https://img.shields.io/github/deployments/psyplot/psyplot-gui/github-pages - :alt: Documentation - :target: http://psyplot.github.io/psyplot-gui/ - -.. |circleci| image:: https://circleci.com/gh/psyplot/psyplot-gui/tree/master.svg?style=svg - :alt: CircleCI - :target: https://circleci.com/gh/psyplot/psyplot-gui/tree/master - -.. |appveyor| image:: https://ci.appveyor.com/api/projects/status/bud4ov6lddrjvt88/branch/master?svg=true - :alt: AppVeyor - :target: https://ci.appveyor.com/project/psyplot/psyplot-gui-q726s - -.. |codecov| image:: https://codecov.io/gh/psyplot/psyplot-gui/branch/master/graph/badge.svg - :alt: Coverage - :target: https://codecov.io/gh/psyplot/psyplot-gui - -.. |version| image:: https://img.shields.io/pypi/v/psyplot-gui.svg?style=flat - :alt: PyPI Package latest release - :target: https://pypi.python.org/pypi/psyplot-gui - -.. |conda| image:: https://anaconda.org/conda-forge/psyplot-gui/badges/version.svg - :alt: conda - :target: https://anaconda.org/conda-forge/psyplot-gui - -.. |supported-versions| image:: https://img.shields.io/pypi/pyversions/psyplot-gui.svg?style=flat - :alt: Supported versions - :target: https://pypi.python.org/pypi/psyplot-gui - -.. |supported-implementations| image:: https://img.shields.io/pypi/implementation/psyplot-gui.svg?style=flat - :alt: Supported implementations - :target: https://pypi.python.org/pypi/psyplot-gui - -.. |zenodo| image:: https://zenodo.org/badge/55793611.svg - :alt: Zenodo - :target: https://zenodo.org/badge/latestdoi/55793611 - -.. |github| image:: https://img.shields.io/github/release/psyplot/psyplot-gui.svg - :target: https://github.com/psyplot/psyplot-gui/releases/latest - :alt: Latest github release +|CI| +|Code coverage| +|Latest Release| +|PyPI version| +|Code style: black| +|Imports: isort| +|PEP8| +|REUSE status| .. end-badges @@ -72,12 +32,21 @@ You can see the full documentation under Copyright --------- -Copyright © 2021 Helmholtz-Zentrum Hereon, 2020-2021 Helmholtz-Zentrum -Geesthacht, 2016-2021 University of Lausanne +Copyright © 2024 Helmholtz-Zentrum Hereon, 2020-2021 Helmholtz-Zentrum +Geesthacht, 2016-2024 University of Lausanne -psyplot-gui is released under the GNU LGPL-3.O license. -See COPYING and COPYING.LESSER in the root of the repository for full -licensing details. +Code files in this repository are licensed under the +LGPL-3.0-only, if not stated otherwise in the file. + +Documentation files in this repository are licensed under CC-BY-4.0, if not +stated otherwise in the file. + +Supplementary and configuration files in this repository are licensed +under CC0-1.0, if not stated otherwise +in the file. + +Please check the header of the individual files for more detailed +information. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as @@ -90,3 +59,20 @@ GNU LGPL-3.0 license for more details. You should have received a copy of the GNU LGPL-3.0 license along with this program. If not, see https://www.gnu.org/licenses/. + +.. |CI| image:: https://codebase.helmholtz.cloud/psyplot/psyplot-gui/badges/main/pipeline.svg + :target: https://codebase.helmholtz.cloud/psyplot/psyplot-gui/-/pipelines?page=1&scope=all&ref=main +.. |Code coverage| image:: https://codebase.helmholtz.cloud/psyplot/psyplot-gui/badges/main/coverage.svg + :target: https://codebase.helmholtz.cloud/psyplot/psyplot-gui/-/graphs/main/charts +.. |Latest Release| image:: https://codebase.helmholtz.cloud/psyplot/psyplot-gui/-/badges/release.svg + :target: https://codebase.helmholtz.cloud/psyplot/psyplot-gui +.. |PyPI version| image:: https://img.shields.io/pypi/v/psyplot-gui.svg + :target: https://pypi.python.org/pypi/psyplot-gui/ +.. |Code style: black| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black +.. |Imports: isort| image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 + :target: https://pycqa.github.io/isort/ +.. |PEP8| image:: https://img.shields.io/badge/code%20style-pep8-orange.svg + :target: https://www.python.org/dev/peps/pep-0008/ +.. |REUSE status| image:: https://api.reuse.software/badge/codebase.helmholtz.cloud/psyplot/psyplot-gui + :target: https://api.reuse.software/info/codebase.helmholtz.cloud/psyplot/psyplot-gui diff --git a/ci/conda-recipe/bld.bat b/ci/conda-recipe/bld.bat deleted file mode 100644 index 52b19de872fa4a81e0c75a1a926f72411ec08d67..0000000000000000000000000000000000000000 --- a/ci/conda-recipe/bld.bat +++ /dev/null @@ -1,11 +0,0 @@ -%PYTHON% -m pip install . --no-deps --ignore-installed -vvv -if errorlevel 1 exit 1 - -set MENU_DIR=%PREFIX%\Menu -IF NOT EXIST (%MENU_DIR%) mkdir %MENU_DIR% - -copy %RECIPE_DIR%\psyplot.ico %MENU_DIR%\psyplot.ico -if errorlevel 1 exit 1 - -copy %RECIPE_DIR%\menu-windows.json %MENU_DIR%\psyplot_shortcut.json -if errorlevel 1 exit 1 diff --git a/ci/conda-recipe/build.sh b/ci/conda-recipe/build.sh deleted file mode 100644 index 614ec75df9ea5c503fd36a2e092ff6190f58733b..0000000000000000000000000000000000000000 --- a/ci/conda-recipe/build.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -$PYTHON -m pip install . --no-deps --ignore-installed -vvv diff --git a/ci/conda-recipe/logo.png b/ci/conda-recipe/logo.png deleted file mode 100644 index b0d09f5a073bb22c470154c39e15f34bb96f708b..0000000000000000000000000000000000000000 Binary files a/ci/conda-recipe/logo.png and /dev/null differ diff --git a/ci/conda-recipe/menu-windows.json b/ci/conda-recipe/menu-windows.json deleted file mode 100644 index b6414a0f577437850afe96b438f79352b71643ad..0000000000000000000000000000000000000000 --- a/ci/conda-recipe/menu-windows.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "menu_name": "Psyplot", - "menu_items": - [ - { - "name": "Psyplot", - "pywscript": "${PYTHON_SCRIPTS}/psyplot-script.py", - "workdir": "${PERSONALDIR}", - "icon": "${MENU_DIR}/psyplot.ico" - } - ] -} diff --git a/ci/conda-recipe/meta.yaml b/ci/conda-recipe/meta.yaml deleted file mode 100644 index ceb8e45e4edc97f595fb710dc344f9732cca51ea..0000000000000000000000000000000000000000 --- a/ci/conda-recipe/meta.yaml +++ /dev/null @@ -1,75 +0,0 @@ -{% set data = load_setup_py_data() %} -{% set name = "psyplot-gui" %} - -package: - name: {{ name|lower }} - version: {{ data.get('version') }} - -source: - git_url: ../.. - -build: - number: {{ environ.get('GIT_DESCRIBE_NUMBER', 0) }} - string: py{{ environ.get('CONDA_PY') }}{% if environ.get("BUILD_STR_END") %}_{{ environ.get("BUILD_STR_END") }}{% endif %} - skip: true # [py == 27] - -requirements: - host: - - python - - pip - run: - - python - - psyplot >=1.3.0 - - pyqt >=5 - - qtconsole - - fasteners - - sphinx - - sphinx_rtd_theme - -test: - imports: - - psyplot_gui - - psyplot_gui.main - - psyplot_gui.compat - - psyplot_gui.compat.qtcompat - - psyplot_gui.config - - psyplot_gui.sphinx_supp - - requires: - - pytest - - pytest-cov - - codecov - - dask - - netcdf4 - - seaborn - - psy-simple - - pip - source_files: - - tests - - commands: - - pip install tests/test_plugin/ - - psyplot --help - - pytest -sv --cov=psyplot_gui - - codecov - -app: - entry: psyplot - icon: logo.png - summary: Psyplot visualization software - type: desk - -about: - home: https://github.com/psyplot/psyplot-gui - license: LGPL-3.0-only - license_family: GPL - license_file: - - COPYING - - COPYING.LESSER - summary: Graphical user interface for the psyplot package - - description: | - This package provides a graphical user interface to interact with the - psyplot framework. - doc_url: http://psyplot.github.io/psyplot-gui - dev_url: https://github.com/psyplot/psyplot-gui diff --git a/ci/conda-recipe/psyplot.ico b/ci/conda-recipe/psyplot.ico deleted file mode 100644 index 441a5d41bee398dc821b3fdb6ad530ebd9b671a9..0000000000000000000000000000000000000000 Binary files a/ci/conda-recipe/psyplot.ico and /dev/null differ diff --git a/ci/environment.yml b/ci/environment.yml deleted file mode 100644 index 0f8588fe2b9c98adb7d7f148552eeaffc2dd04b6..0000000000000000000000000000000000000000 --- a/ci/environment.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: psyplot_gui_docs -channels: - - local - - psyplot/label/master - - conda-forge -dependencies: - - python=3.8 - - scipy - - netcdf4=1.5.7 - - h5py=3.4.0 - - hdf5=1.12.1 - - pytest-qt - - psy-simple - - psyplot-gui - - pytest-cov - - psutil - - pip - - pip: - - ../tests/test_plugin/ \ No newline at end of file diff --git a/ci/matrix/default/Pipfile b/ci/matrix/default/Pipfile new file mode 100644 index 0000000000000000000000000000000000000000..f91afd7a0b88063ec4024effad8b3fc7492ee720 --- /dev/null +++ b/ci/matrix/default/Pipfile @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +psyplot-gui = {extras = ["testsite"], file = "../../.."} +psyplot = {ref = "develop", git = "git+https://codebase.helmholtz.cloud/psyplot/psyplot.git"} +psy-simple = {ref = "develop", git = "git+https://codebase.helmholtz.cloud/psyplot/psy-simple.git"} +matplotlib = "3.7.*" +PyQt5 = {version="*"} +PyQtWebEngine = {version="*"} +psyplot_gui_test = {path = "../../../tests/test_plugin/"} + + +[dev-packages] + +[pipenv] +allow_prereleases = true + +[requires] +python_version = "3.9" diff --git a/docs/Makefile b/docs/Makefile index 191ffff6042c55b326210e8138dc453843537c86..b1567a16b2b6575374d00f2b0f7c078437060905 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,192 +1,24 @@ -# Makefile for Sphinx documentation +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # +# SPDX-License-Identifier: CC0-1.0 -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# Minimal makefile for Sphinx documentation +# -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make <target>' where <target> is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/syplot.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/syplot.qhc" - -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/syplot" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/syplot" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." +.PHONY: help Makefile -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/.gitignore b/docs/_static/.gitignore deleted file mode 100644 index 91fd5883327eef83f963ddc41a4c9e1f4e01233b..0000000000000000000000000000000000000000 --- a/docs/_static/.gitignore +++ /dev/null @@ -1 +0,0 @@ -docs_*.png diff --git a/docs/_static/license_logo.png b/docs/_static/license_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c8473a24786ab016d9c3e717a380910f7cbb0fff Binary files /dev/null and b/docs/_static/license_logo.png differ diff --git a/docs/_static/license_logo.png.license b/docs/_static/license_logo.png.license new file mode 100644 index 0000000000000000000000000000000000000000..0329700b91c67e9dd088e6257f03d2c42272c096 --- /dev/null +++ b/docs/_static/license_logo.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Creative Commons + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/_templates/footer.html b/docs/_templates/footer.html new file mode 100644 index 0000000000000000000000000000000000000000..3e570797e7d14fc39ee78e764ec16ed00ebefbb5 --- /dev/null +++ b/docs/_templates/footer.html @@ -0,0 +1,22 @@ +<!-- +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC-BY-4.0 +--> + +{% extends "!footer.html" %} +{% block extrafooter %} + +<div class="footer"> + <span class="creativecommons"> + <a href="https://creativecommons.org/licenses/by/4.0/" > + <img style="height: 1.5em;" src="{{ pathto('_static/license_logo.png', 1) }}" border="0" alt="Creative Commons License"/> + </a> + This documentation is licensed under + <a href="https://creativecommons.org/licenses/by/4.0/"> + CC-BY-4.0 + </a> + </span> +</div> + +{% endblock %} diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000000000000000000000000000000000000..b71d6a8cc9d9c3f543f3f1d562b204706d7ab071 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,13 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + +.. _api: + +API Reference +============= + + +.. toctree:: + + api/psyplot_gui diff --git a/docs/apigen.bash b/docs/apigen.bash deleted file mode 100755 index d3344c68d524162298dc23bf59cf99ff29a3ad67..0000000000000000000000000000000000000000 --- a/docs/apigen.bash +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -e -# script to automatically generate the psyplot api documentation using -# sphinx-apidoc and sed -sphinx-apidoc -f -M -e -T -o api ../psyplot_gui/ -# replace chapter title in psyplot.rst -sed -i -e '1,1s/.*/API Reference/' api/psyplot_gui.rst - -sphinx-autogen -o generated *.rst */*.rst diff --git a/docs/changelog.rst b/docs/changelog.rst index c393d07bb680e5e17a64cfc99e9a9ad874920af6..e981bbd3d87aeff0bbc462b56cfe5ec290028951 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,3 +1,7 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + .. _changelog: Changelog diff --git a/docs/command_line.rst b/docs/command_line.rst index 81c39af3e0f450b2ecb4e3d6805c9eadadc9d158..cff38d24fb5b78c4ee056679cb78bffb65276f26 100644 --- a/docs/command_line.rst +++ b/docs/command_line.rst @@ -1,3 +1,7 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + .. _command-line: Command line usage diff --git a/docs/conf.py b/docs/conf.py index ecb759d016f9aba4cbc39a9691d4d9fe8a1573a2..f6e49efb92ebe20caa31836a5f646f02fbd624e8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,205 +1,144 @@ -# -*- coding: utf-8 -*- +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# psyplot documentation build configuration file, created by -# sphinx-quickstart on Mon Jul 20 18:01:33 2015. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. +# SPDX-License-Identifier: LGPL-3.0-only + +# Configuration file for the Sphinx documentation builder. # -# All configuration values have a default; values that are commented out -# serve to show the default. +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html -import sphinx +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# import os -import os.path as osp -import subprocess as spr -import re -import six +import sys from itertools import product -import psyplot_gui -import matplotlib as mpl -import warnings +from pathlib import Path +from sphinx.ext import apidoc -warnings.filterwarnings( - 'ignore', message=r"\s*The Panel class is removed from pandas") +sys.path.insert(0, os.path.abspath("..")) +if not os.path.exists("_static"): + os.makedirs("_static") -mpl.use('agg') +# isort: off -# -- General configuration ------------------------------------------------ +import psyplot_gui -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.autosummary', - 'sphinx.ext.todo', - 'sphinx.ext.viewcode', - 'matplotlib.sphinxext.plot_directive', - 'IPython.sphinxext.ipython_console_highlighting', - 'IPython.sphinxext.ipython_directive', - 'sphinxarg.ext', - 'psyplot.sphinxext.extended_napoleon', - 'autodocsumm', -] +# isort: on -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] -# create the api documentation -if not osp.exists(osp.join(osp.dirname(__file__), 'api')): - spr.check_call(['bash', 'apigen.bash']) +def generate_apidoc(app): + appdir = Path(app.__file__).parent + apidoc.main( + ["-fMEeTo", str(api), str(appdir), str(appdir / "migrations" / "*")] + ) -napoleon_use_admonition_for_examples = True -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -source_suffix = '.rst' +api = Path("api") -# The master toctree document. -master_doc = 'index' +if not api.exists(): + generate_apidoc(psyplot_gui) -autodoc_default_options = { - 'show_inheritance': True, - 'autosummary': True, -} +# -- Project information ----------------------------------------------------- -autoclass_content = 'both' +project = "psyplot-gui" +copyright = "2021-2024 Helmholtz-Zentrum hereon GmbH" +author = "Philipp S. Sommer" -not_document_data = ['psyplot_gui.config.rcsetup.defaultParams', - 'psyplot_gui.config.rcsetup.rcParams'] -ipython_savefig_dir = os.path.join(os.path.dirname(__file__), '_static') +linkcheck_ignore = [ + # we do not check link of the psyplot-gui as the + # badges might not yet work everywhere. Once psyplot-gui + # is settled, the following link should be removed + r"https://.*psyplot-gui" +] -# General information about the project. -project = 'psyplot-gui' -copyright = ", ".join( - psyplot_gui.__copyright__.strip().replace("Copyright (C) ", "").splitlines() -) -author = psyplot_gui.__author__ -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = re.match(r'\d+\.\d+\.\d+', psyplot_gui.__version__).group() -# The full version, including alpha/beta/rc tags. -release = psyplot_gui.__version__ +# -- General configuration --------------------------------------------------- -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "hereon_nc_sphinxext", + "sphinx.ext.intersphinx", + "sphinx_design", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "autodocsumm", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "matplotlib.sphinxext.plot_directive", + "IPython.sphinxext.ipython_console_highlighting", + "IPython.sphinxext.ipython_directive", + "sphinxarg.ext", + "psyplot.sphinxext.extended_napoleon", +] + + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True +autodoc_default_options = { + "show_inheritance": True, + "members": True, + "autosummary": True, +} -# -- Options for HTML output ---------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. +html_theme = "sphinx_rtd_theme" -html_theme = 'sphinx_rtd_theme' +html_theme_options = { + "collapse_navigation": False, + "includehidden": False, +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Output file base name for HTML help builder. -htmlhelp_basename = 'psyplotguidoc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # Additional stuff for the LaTeX preamble. - 'preamble': '\setcounter{tocdepth}{10}' -} +html_static_path = ["_static"] -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'psyplot-gui.tex', u'psyplot GUI Documentation', - author, 'manual'), -] - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'psyplot-gui', u'psyplot GUI Documentation', - [author], 1) -] - -# -- Options for Texinfo output ------------------------------------------- -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'psyplot-gui', u'psyplot GUI Documentation', - author, 'psyplot-gui', 'Graphical user interface for the psyplot package', - 'Visualization'), -] - -# -- Options for Epub output ---------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project -epub_author = author -epub_publisher = author -epub_copyright = copyright - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] - -# Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), - 'numpy': ('https://docs.scipy.org/doc/numpy/', None), - 'matplotlib': ('https://matplotlib.org/stable/', None), - 'sphinx': ('https://www.sphinx-doc.org/en/stable/', None), - 'xarray': ('https://xarray.pydata.org/en/stable/', None), - 'cartopy': ('https://scitools.org.uk/cartopy/docs/latest/', None), - 'mpl_toolkits': ('https://matplotlib.org/basemap/', None), - 'psyplot': ('https://psyplot.github.io/psyplot/', None), - 'python': ('https://docs.python.org/3/', None), + "python": ("https://docs.python.org/3/", None), + "matplotlib": ("https://matplotlib.org/stable/", None), + "sphinx": ("https://www.sphinx-doc.org/en/stable/", None), + "xarray": ("https://xarray.pydata.org/en/stable/", None), + "cartopy": ("https://scitools.org.uk/cartopy/docs/latest/", None), + "psyplot": ("https://psyplot.github.io/psyplot/", None), } replacements = { - '`psyplot.rcParams`': '`~psyplot.config.rcsetup.rcParams`', - '`psyplot.InteractiveList`': '`~psyplot.data.InteractiveList`', - '`psyplot.InteractiveArray`': '`~psyplot.data.InteractiveArray`', - '`psyplot.open_dataset`': '`~psyplot.data.open_dataset`', - '`psyplot.open_mfdataset`': '`~psyplot.data.open_mfdataset`', - } + "`psyplot.rcParams`": "`~psyplot.config.rcsetup.rcParams`", + "`psyplot.InteractiveList`": "`~psyplot.data.InteractiveList`", + "`psyplot.InteractiveArray`": "`~psyplot.data.InteractiveArray`", + "`psyplot.open_dataset`": "`~psyplot.data.open_dataset`", + "`psyplot.open_mfdataset`": "`~psyplot.data.open_mfdataset`", +} def link_aliases(app, what, name, obj, options, lines): - for (key, val), (i, line) in product(six.iteritems(replacements), - enumerate(lines)): + for (key, val), (i, line) in product( + replacements.items(), enumerate(lines) + ): lines[i] = line.replace(key, val) def setup(app): - app.connect('autodoc-process-docstring', link_aliases) - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} + app.connect("autodoc-process-docstring", link_aliases) diff --git a/docs/configuration.rst b/docs/configuration.rst index 238dac362828132179a0d21e90361485b2c6722e..acc827f2b4b5f5b72a5c9d8cc738fa4c74b1bc36 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -1,3 +1,7 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + .. _configuration: Configuration of the GUI diff --git a/docs/contribute.rst b/docs/contribute.rst deleted file mode 100644 index ba0163d34414a79d33c5b650fe36b80b922ef6c4..0000000000000000000000000000000000000000 --- a/docs/contribute.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _how-to-contribute: - -Contributing to psyplot-gui -=========================== - -First off, thanks for taking the time to contribute! - -Please see the section about :ref:`contributing to psyplot <psyplot:how-to-contribute>` for details. diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 0000000000000000000000000000000000000000..4ef08d97fe752a523b72fc43d70a4426be71c39e --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1,10 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + +.. _contributing: + +Contribution and development hints +================================== + +See :ref:`psyplots contribution guidelines <psyplot:contributing>`. diff --git a/docs/environment.yml b/docs/environment.yml deleted file mode 100644 index 7cd0fa5d193c65c2b495a8c27924e59134918744..0000000000000000000000000000000000000000 --- a/docs/environment.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: psyplot_gui_docs -channels: - - local - - psyplot/label/master - - conda-forge -dependencies: - - python=3.8 - - qtconsole - - dask - - netcdf4 - - sphinx_rtd_theme - - ipykernel - - pexpect - - pygments - - pyqt - - sphinx - - psy-simple - - psyplot-gui - - autodocsumm - - fasteners - - sphinx-argparse diff --git a/docs/explained_screenshot.pdf.license b/docs/explained_screenshot.pdf.license new file mode 100644 index 0000000000000000000000000000000000000000..b21fae9960bf621e598d171baf4de286e59ac4e1 --- /dev/null +++ b/docs/explained_screenshot.pdf.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/explained_screenshot.png.license b/docs/explained_screenshot.png.license new file mode 100644 index 0000000000000000000000000000000000000000..b21fae9960bf621e598d171baf4de286e59ac4e1 --- /dev/null +++ b/docs/explained_screenshot.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 4ad7d52137db5d0c6a6a810555bfce5dbe8ec908..04aed2ad362d1a81d44299d6b765aa84132074ee 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -1,3 +1,7 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + .. _getting-started: Getting started diff --git a/docs/index.rst b/docs/index.rst index 1a785df7987afd8c6e6c6816d69e1bc2604c5285..2602d6ecc353c887e1d572bcfc48cc9a20fbe732 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,110 +1,88 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + .. psyplot-gui documentation master file + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to psyplot-gui's documentation! +======================================= -.. _psyplot-gui: +|CI| +|Code coverage| +|Latest Release| +|PyPI version| +|Code style: black| +|Imports: isort| +|PEP8| +|REUSE status| -Graphical User Interface for the psyplot package -================================================ +.. rubric:: Graphical user interface for the psyplot package Welcome! This package enhances the interactive visualization framework psyplot_ with a graphical user interface using PyQt_. See the homepage of the main project on examples on the possibilities with psyplot_. -.. only:: html and not epub - - .. list-table:: - :stub-columns: 1 - :widths: 10 90 - - * - docs - - |docs| - * - tests - - |circleci| |appveyor| |codecov| - * - package - - |version| |conda| |github| |zenodo| - * - implementations - - |supported-versions| |supported-implementations| - - .. |docs| image:: https://img.shields.io/github/deployments/psyplot/psyplot-gui/github-pages - :alt: Documentation - :target: http://psyplot.github.io/psyplot-gui/ +.. _PyQt: https://riverbankcomputing.com/software/pyqt/intro +.. _psyplot: https://psyplot.github.io/psyplot/ - .. |circleci| image:: https://circleci.com/gh/psyplot/psyplot-gui/tree/master.svg?style=svg - :alt: CircleCI - :target: https://circleci.com/gh/psyplot/psyplot-gui/tree/master +.. toctree:: + :maxdepth: 1 - .. |appveyor| image:: https://ci.appveyor.com/api/projects/status/bud4ov6lddrjvt88/branch/master?svg=true - :alt: AppVeyor - :target: https://ci.appveyor.com/project/psyplot/psyplot-gui-q726s + installing + getting_started + configuration + command_line + plugins + contributing + api + changelog + todos - .. |codecov| image:: https://codecov.io/gh/psyplot/psyplot-gui/branch/master/graph/badge.svg - :alt: Coverage - :target: https://codecov.io/gh/psyplot/psyplot-gui - .. |version| image:: https://img.shields.io/pypi/v/psyplot-gui.svg?style=flat - :alt: PyPI Package latest release - :target: https://pypi.python.org/pypi/psyplot-gui +How to cite this software +------------------------- - .. |conda| image:: https://anaconda.org/conda-forge/psyplot-gui/badges/version.svg - :alt: conda - :target: https://anaconda.org/conda-forge/psyplot-gui +.. card:: Please do cite this software! - .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/psyplot-gui.svg?style=flat - :alt: Supported versions - :target: https://pypi.python.org/pypi/psyplot-gui + .. tab-set:: - .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/psyplot-gui.svg?style=flat - :alt: Supported implementations - :target: https://pypi.python.org/pypi/psyplot-gui + .. tab-item:: APA - .. |zenodo| image:: https://zenodo.org/badge/55793611.svg - :alt: Zenodo - :target: https://zenodo.org/badge/latestdoi/55793611 + .. citation-info:: + :format: apalike - .. |github| image:: https://img.shields.io/github/release/psyplot/psyplot-gui.svg - :target: https://github.com/psyplot/psyplot-gui/releases/latest - :alt: Latest github release + .. tab-item:: BibTex + .. citation-info:: + :format: bibtex -Documentation -------------- + .. tab-item:: RIS -.. toctree:: - :maxdepth: 1 + .. citation-info:: + :format: ris - installing - getting_started - configuration - command_line - plugins - contribute - api/psyplot_gui - changelog - todos + .. tab-item:: Endnote -.. _PyQt: https://riverbankcomputing.com/software/pyqt/intro -.. _psyplot: https://psyplot.github.io/psyplot/ + .. citation-info:: + :format: endnote + .. tab-item:: CFF -Copyright ---------- -Copyright © 2021 Helmholtz-Zentrum Hereon, 2020-2021 Helmholtz-Zentrum -Geesthacht, 2016-2021 University of Lausanne + .. citation-info:: + :format: cff -psyplot-gui is released under the GNU LGPL-3.O license. -See COPYING and COPYING.LESSER in the root of the repository for full -licensing details. -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License version 3.0 as -published by the Free Software Foundation. +License information +------------------- +Copyright © 2021-2024 Helmholtz-Zentrum hereon GmbH -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 LGPL-3.0 license for more details. +The source code of psyplot-gui is licensed under +LGPL-3.0-only. -You should have received a copy of the GNU LGPL-3.0 license -along with this program. If not, see https://www.gnu.org/licenses/. +If not stated otherwise, the contents of this documentation is licensed under +CC-BY-4.0. Indices and tables @@ -113,3 +91,22 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` + + +.. |CI| image:: https://codebase.helmholtz.cloud/psyplot/psyplot-gui/badges/main/pipeline.svg + :target: https://codebase.helmholtz.cloud/psyplot/psyplot-gui/-/pipelines?page=1&scope=all&ref=main +.. |Code coverage| image:: https://codebase.helmholtz.cloud/psyplot/psyplot-gui/badges/main/coverage.svg + :target: https://codebase.helmholtz.cloud/psyplot/psyplot-gui/-/graphs/main/charts +.. |Latest Release| image:: https://codebase.helmholtz.cloud/psyplot/psyplot-gui/-/badges/release.svg + :target: https://codebase.helmholtz.cloud/psyplot/psyplot-gui +.. |PyPI version| image:: https://img.shields.io/pypi/v/psyplot-gui.svg + :target: https://pypi.python.org/pypi/psyplot-gui/ +.. |Code style: black| image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black +.. |Imports: isort| image:: https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336 + :target: https://pycqa.github.io/isort/ +.. |PEP8| image:: https://img.shields.io/badge/code%20style-pep8-orange.svg + :target: https://www.python.org/dev/peps/pep-0008/ +.. TODO: uncomment the following line when the package is registered at https://api.reuse.software +.. .. |REUSE status| image:: https://api.reuse.software/badge/codebase.helmholtz.cloud/psyplot/psyplot-gui +.. :target: https://api.reuse.software/info/codebase.helmholtz.cloud/psyplot/psyplot-gui diff --git a/docs/installing.rst b/docs/installing.rst index f127945e09512d22d663e3ba5f4b7edf96a8d3c0..01ca6e346a60bfe152b60d84241652cf63a11dce 100644 --- a/docs/installing.rst +++ b/docs/installing.rst @@ -1,3 +1,7 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + .. _install: .. highlight:: bash diff --git a/docs/ipython_console.png.license b/docs/ipython_console.png.license new file mode 100644 index 0000000000000000000000000000000000000000..b21fae9960bf621e598d171baf4de286e59ac4e1 --- /dev/null +++ b/docs/ipython_console.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000000000000000000000000000000000000..2e7d9b0701cbcc0b774d0d1b61ff1646f990e0cf --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,39 @@ +REM SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +REM +REM SPDX-License-Identifier: CC0-1.0 + +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/plugins.rst b/docs/plugins.rst index 8d6da627f5fec39a488bd16c63dd2976596836c1..6153e28b67e139937e90d24b1eb8e92614e68081 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -1,3 +1,7 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + .. currentmodule:: psyplot_gui.main .. _plugins: @@ -34,13 +38,17 @@ script of a package, include the following: .. code-block:: python - setup(..., - entry_points={'psyplot_gui': [ - 'widget-name1=widget-module1:widget-class-name1', - 'widget-name2=widget-module2:widget-class-name2', - ...], - }, - ...) + setup( + ..., + entry_points={ + "psyplot_gui": [ + "widget-name1=widget-module1:widget-class-name1", + "widget-name2=widget-module2:widget-class-name2", + ..., + ], + }, + ..., + ) Here, `widget-name1` is an arbitrary name you want to assign to the widget, `widget-module1` is the module from where to import the plugin, and @@ -51,9 +59,12 @@ For the :attr:`~MainWindow.help_explorer`, this, for example, would like like .. code-block:: python - setup(..., - entry_points={'psyplot_gui': [ - 'help=psyplot_gui.help_explorer:HelpExplorer', - ], - }, - ...) + setup( + ..., + entry_points={ + "psyplot_gui": [ + "help=psyplot_gui.help_explorer:HelpExplorer", + ], + }, + ..., + ) diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..6aabf6d571aa2242ec760647a7bae79aa18a2dff --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +sphinx-design +git+https://codebase.helmholtz.cloud/hcdc/hereon-netcdf/sphinxext.git +git+https://codebase.helmholtz.cloud/psyplot/psyplot.git@develop +git+https://codebase.helmholtz.cloud/psyplot/psy-simple.git@develop diff --git a/docs/todos.rst b/docs/todos.rst index 5b6c5dc8d45296d6f167c090dc73cd46edc4ad78..f37b1ff69aa7b206cfa04ea78b10d6018793ede4 100644 --- a/docs/todos.rst +++ b/docs/todos.rst @@ -1,3 +1,7 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + ToDos ===== diff --git a/docs/world.png b/docs/world.png index 41ca13a9e88f3a4e0d297bffae44ea270fcc8aaf..86838e1cb53a9526ff2b04ce4181ec54c5e08dbd 100644 Binary files a/docs/world.png and b/docs/world.png differ diff --git a/docs/world.png.license b/docs/world.png.license new file mode 100644 index 0000000000000000000000000000000000000000..b21fae9960bf621e598d171baf4de286e59ac4e1 --- /dev/null +++ b/docs/world.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/__init__.py b/psyplot_gui/__init__.py index 31908a1ebe7d0e102fbdd03154e8315a2f5224dd..a6777ecfcd1b66f2867c4505df4a02beef61d926 100644 --- a/psyplot_gui/__init__.py +++ b/psyplot_gui/__init__.py @@ -1,79 +1,63 @@ -"""Core package for the psyplot graphical user interface""" - -# Disclaimer -# ---------- -# -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only -import sys +"""psyplot-gui + +Graphical user interface for the psyplot package +""" + +from __future__ import annotations + +import argparse +import atexit +import datetime as dt +import logging import os import os.path as osp -import six -import tempfile -import yaml +import pickle import socket -import atexit -import fasteners +import sys +import tempfile import time -import pickle -import datetime as dt -import logging -import argparse -import xarray as xr +from itertools import chain + +import fasteners import psyplot +import six +import xarray as xr +import yaml from psyplot.__main__ import make_plot -from psyplot_gui.config.rcsetup import rcParams -import psyplot_gui.config as config -from itertools import chain from psyplot.config.rcsetup import get_configdir, safe_list from psyplot.docstring import docstrings +from psyplot.utils import get_default_value from psyplot.warning import warn -from psyplot.compat.pycompat import map - -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions +import psyplot_gui.config as config +from psyplot_gui.config.rcsetup import rcParams +from . import _version -from psyplot.compat.pycompat import get_default_value +__version__ = _version.get_versions()["version"] __author__ = "Philipp S. Sommer" -__copyright__ = """ -Copyright (C) 2021 Helmholtz-Zentrum Hereon -Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -Copyright (C) 2016-2021 University of Lausanne -""" -__credits__ = ["Philipp S. Sommer"] +__copyright__ = "2021-2024 Helmholtz-Zentrum hereon GmbH" +__credits__ = [ + "Philipp S. Sommer", +] __license__ = "LGPL-3.0-only" __maintainer__ = "Philipp S. Sommer" -__email__ = "psyplot@hereon.de" +__email__ = "philipp.sommer@hereon.de" __status__ = "Production" logger = logging.getLogger(__name__) logger.debug( "%s: Initializing psyplot gui, version %s", - dt.datetime.now().isoformat(), __version__) + dt.datetime.now().isoformat(), + __version__, +) logger.debug("psyplot version: %s", psyplot.__version__) logger.debug("Logging configuration file: %s", config.logcfg_path) logger.debug("Configuration file: %s", config.config_path) @@ -86,39 +70,60 @@ logger = logging.getLogger(__name__) def get_versions(requirements=True): - ret = {'version': __version__} + ret = {"version": __version__} if requirements: - req = ret['requirements'] = {} + req = ret["requirements"] = {} try: import qtconsole except Exception: - logger.error('Could not load qtconsole!', exc_info=True) + logger.error("Could not load qtconsole!", exc_info=True) else: - req['qtconsole'] = qtconsole.__version__ + req["qtconsole"] = qtconsole.__version__ try: from psyplot_gui.compat.qtcompat import PYQT_VERSION, QT_VERSION except Exception: - logger.error('Could not load qt and pyqt!', exc_info=True) + logger.error("Could not load qt and pyqt!", exc_info=True) else: - req['qt'] = QT_VERSION - req['pyqt'] = PYQT_VERSION + req["qt"] = QT_VERSION + req["pyqt"] = PYQT_VERSION return ret -@docstrings.get_sections(base='psyplot_gui.start_app') +@docstrings.get_sections(base="psyplot_gui.start_app") @docstrings.dedent -def start_app(fnames=[], name=[], dims=None, plot_method=None, - output=None, project=None, engine=None, formatoptions=None, - tight=False, encoding=None, enable_post=False, - seaborn_style=None, output_project=None, - concat_dim=get_default_value(xr.open_mfdataset, 'concat_dim'), - chname={}, - backend=False, new_instance=False, rc_file=None, - rc_gui_file=None, include_plugins=rcParams['plugins.include'], - exclude_plugins=rcParams['plugins.exclude'], offline=False, - pwd=None, script=None, command=None, exec_=True, use_all=False, - callback=None, preset=None, - opengl_implementation=None, webengineview=True): +def start_app( + fnames=[], + name=[], + dims=None, + plot_method=None, + output=None, + project=None, + engine=None, + formatoptions=None, + tight=False, + encoding=None, + enable_post=False, + seaborn_style=None, + output_project=None, + concat_dim=get_default_value(xr.open_mfdataset, "concat_dim"), + chname={}, + backend=False, + new_instance=False, + rc_file=None, + rc_gui_file=None, + include_plugins=rcParams["plugins.include"], + exclude_plugins=rcParams["plugins.exclude"], + offline=False, + pwd=None, + script=None, + command=None, + exec_=True, + use_all=False, + callback=None, + preset=None, + opengl_implementation=None, + webengineview=True, +): """ Eventually start the QApplication or only make a plot @@ -186,36 +191,50 @@ def start_app(fnames=[], name=[], dims=None, plot_method=None, script = osp.abspath(script) if project is not None and (name != [] or dims is not None): - warn('The `name` and `dims` parameter are ignored if the `project`' - ' parameter is set!') + warn( + "The `name` and `dims` parameter are ignored if the `project`" + " parameter is set!" + ) # load rcParams from file if rc_gui_file is not None: rcParams.load_from_file(rc_gui_file) # set plugins - rcParams['plugins.include'] = include_plugins - rcParams['plugins.exclude'] = exclude_plugins + rcParams["plugins.include"] = include_plugins + rcParams["plugins.exclude"] = exclude_plugins if webengineview is not None: - rcParams['help_explorer.use_webengineview'] = webengineview + rcParams["help_explorer.use_webengineview"] = webengineview if offline: - rcParams['help_explorer.online'] = False - rcParams['help_explorer.use_intersphinx'] = False + rcParams["help_explorer.online"] = False + rcParams["help_explorer.use_intersphinx"] = False if dims is not None and not isinstance(dims, dict): dims = dict(chain(*map(six.iteritems, dims))) if output is not None: return make_plot( - fnames=fnames, name=name, dims=dims, plot_method=plot_method, - output=output, project=project, engine=engine, - formatoptions=formatoptions, tight=tight, rc_file=rc_file, - encoding=encoding, enable_post=enable_post, - seaborn_style=seaborn_style, output_project=output_project, - concat_dim=concat_dim, chname=chname, preset=preset) + fnames=fnames, + name=name, + dims=dims, + plot_method=plot_method, + output=output, + project=project, + engine=engine, + formatoptions=formatoptions, + tight=tight, + rc_file=rc_file, + encoding=encoding, + enable_post=enable_post, + seaborn_style=seaborn_style, + output_project=output_project, + concat_dim=concat_dim, + chname=chname, + preset=preset, + ) if use_all: - name = 'all' + name = "all" else: name = safe_list(name) @@ -227,22 +246,26 @@ def start_app(fnames=[], name=[], dims=None, plot_method=None, formatoptions = formatoptions[0] if preset is not None: import psyplot.project as psy + preset_data = psy.Project._load_preset(preset) else: preset_data = {} preset_data.update(formatoptions) - preset = tempfile.NamedTemporaryFile(prefix='psy_', suffix='.yml').name - with open(preset, 'w') as f: + preset = tempfile.NamedTemporaryFile(prefix="psy_", suffix=".yml").name + with open(preset, "w") as f: yaml.dump(preset_data, f) # make preset path absolute - if preset is not None and not isinstance(preset, dict) and \ - osp.exists(preset): + if ( + preset is not None + and not isinstance(preset, dict) + and osp.exists(preset) + ): preset = osp.abspath(preset) # Lock file creation if not new_instance: - lock_file = osp.join(get_configdir(), 'psyplot.lock') + lock_file = osp.join(get_configdir(), "psyplot.lock") lock = fasteners.InterProcessLock(lock_file) # Try to lock psyplot.lock. If it's *possible* to do it, then @@ -261,32 +284,45 @@ def start_app(fnames=[], name=[], dims=None, plot_method=None, elif not new_instance: if callback is None: if fnames or project: - callback = 'new_plot' + callback = "new_plot" elif pwd is not None: - callback = 'change_cwd' + callback = "change_cwd" fnames = [pwd] elif script is not None: - callback = 'run_script' + callback = "run_script" fnames = [script] elif command is not None: - callback = 'command' + callback = "command" engine = command if callback: send_files_to_psyplot( - callback, fnames, project, engine, plot_method, name, dims, - encoding, enable_post, seaborn_style, concat_dim, chname, - preset) + callback, + fnames, + project, + engine, + plot_method, + name, + dims, + encoding, + enable_post, + seaborn_style, + concat_dim, + chname, + preset, + ) return elif new_instance: - rcParams['main.listen_to_port'] = False + rcParams["main.listen_to_port"] = False if backend is not False: - rcParams['backend'] = backend + rcParams["backend"] = backend from psyplot_gui.main import MainWindow + fnames = _get_abs_names(fnames) if project is not None: project = _get_abs_names([project])[0] if exec_: from psyplot_gui.compat.qtcompat import QApplication + app = QApplication(sys.argv) _set_opengl_implementation(opengl_implementation) @@ -294,9 +330,20 @@ def start_app(fnames=[], name=[], dims=None, plot_method=None, if isinstance(new_instance, MainWindow): mainwindow = new_instance else: - mainwindow = MainWindow.run(fnames, project, engine, plot_method, name, - dims, encoding, enable_post, seaborn_style, - concat_dim, chname, preset) + mainwindow = MainWindow.run( + fnames, + project, + engine, + plot_method, + name, + dims, + encoding, + enable_post, + seaborn_style, + concat_dim, + chname, + preset, + ) if script is not None: mainwindow.console.run_script_in_shell(script) if command is not None: @@ -315,7 +362,7 @@ def send_files_to_psyplot(callback, fnames, project, *args): This function has to most parts been taken from spyder """ - port = rcParams['main.open_files_port'] + port = rcParams["main.open_files_port"] # Wait ~50 secs for the server to be up # Taken from http://stackoverflow.com/a/4766598/438386 @@ -324,8 +371,9 @@ def send_files_to_psyplot(callback, fnames, project, *args): if project is not None: project = _get_abs_names([project])[0] try: - client = socket.socket(socket.AF_INET, socket.SOCK_STREAM, - socket.IPPROTO_TCP) + client = socket.socket( + socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP + ) client.connect(("127.0.0.1", port)) client.send(pickle.dumps([callback, fnames, project] + list(args))) client.close() @@ -341,7 +389,7 @@ def _get_abs_names(fnames): return for i, fname in enumerate(fnames): if fname: - fnames[i] = ','.join(map(osp.abspath, fname.split(','))) + fnames[i] = ",".join(map(osp.abspath, fname.split(","))) return fnames @@ -360,74 +408,103 @@ def get_parser(create=True): psyplot.parser.FuncArgParser psyplot.main.main""" from psyplot.__main__ import get_parser + parser = get_parser(create=False) parser.setup_args(start_app) gui_grp = parser.add_argument_group( - 'Gui options', - 'Options specific to the graphical user interface') + "Gui options", "Options specific to the graphical user interface" + ) parser.update_arg( - 'backend', short='b', const=None, nargs='?', metavar='backend', + "backend", + short="b", + const=None, + nargs="?", + metavar="backend", help=""" The backend to use. By default, the ``'gui.backend'`` key in the :attr:`~psyplot_gui.config.rcsetup.rcParams` dictionary is used. If used without options, the default matplotlib backend is used.""", - group=gui_grp) + group=gui_grp, + ) - parser.update_arg('new_instance', short='ni', group=gui_grp) + parser.update_arg("new_instance", short="ni", group=gui_grp) - parser.update_arg('rc_gui_file', short='rc-gui', group=gui_grp) - parser.pop_key('rc_gui_file', 'metavar') - parser.update_arg('include_plugins', short='inc', group=gui_grp, - default=rcParams['plugins.include']) - parser.append2help('include_plugins', '. Default: %(default)s') - parser.update_arg('exclude_plugins', short='exc', group=gui_grp, - default=rcParams['plugins.exclude']) - parser.append2help('exclude_plugins', '. Default: %(default)s') - - parser.update_arg('offline', group=gui_grp) - parser.update_arg('pwd', group=gui_grp) - parser.update_arg('script', short='s', group=gui_grp) - parser.update_arg('command', short='c', group=gui_grp) + parser.update_arg("rc_gui_file", short="rc-gui", group=gui_grp) + parser.pop_key("rc_gui_file", "metavar") + parser.update_arg( + "include_plugins", + short="inc", + group=gui_grp, + default=rcParams["plugins.include"], + ) + parser.append2help("include_plugins", ". Default: %(default)s") + parser.update_arg( + "exclude_plugins", + short="exc", + group=gui_grp, + default=rcParams["plugins.exclude"], + ) + parser.append2help("exclude_plugins", ". Default: %(default)s") + + parser.update_arg("offline", group=gui_grp) + parser.update_arg("pwd", group=gui_grp) + parser.update_arg("script", short="s", group=gui_grp) + parser.update_arg("command", short="c", group=gui_grp) - parser.update_arg('opengl_implementation', group=gui_grp, short='opengl', - choices=['software', 'desktop', 'gles', 'automatic']) + parser.update_arg( + "opengl_implementation", + group=gui_grp, + short="opengl", + choices=["software", "desktop", "gles", "automatic"], + ) # add an action to display the GUI plugins - info_grp = parser.unfinished_arguments['list_plugins'].get('group') + info_grp = parser.unfinished_arguments["list_plugins"].get("group") parser.update_arg( - 'list_gui_plugins', short='lgp', long='list-gui-plugins', - action=ListGuiPluginsAction, if_existent=False, - help=("Print the names of the GUI plugins and exit. Note that the " - "displayed plugins are not affected by the `include-plugins` " - "and `exclude-plugins` options")) + "list_gui_plugins", + short="lgp", + long="list-gui-plugins", + action=ListGuiPluginsAction, + if_existent=False, + help=( + "Print the names of the GUI plugins and exit. Note that the " + "displayed plugins are not affected by the `include-plugins` " + "and `exclude-plugins` options" + ), + ) if info_grp is not None: - parser.unfinished_arguments['list_gui_plugins']['group'] = info_grp - - parser.pop_key('offline', 'short') + parser.unfinished_arguments["list_gui_plugins"]["group"] = info_grp - parser.append2help('output_project', - '. This option has only an effect if the `output` ' - ' option is set.') + parser.pop_key("offline", "short") - parser.update_arg('use_all', short='a') + parser.append2help( + "output_project", + ". This option has only an effect if the `output` " " option is set.", + ) - parser.pop_arg('exec_') - parser.pop_arg('callback') + parser.update_arg("use_all", short="a") - parser.pop_key('webengineview', 'short') - parser.update_arg('webengineview', default=None, action='store_true', - group=gui_grp) + parser.pop_arg("exec_") + parser.pop_arg("callback") - parser.unfinished_arguments['no-webengineview'] = dict( - long='no-webengineview', default=None, action='store_false', - dest='webengineview', + parser.pop_key("webengineview", "short") + parser.update_arg( + "webengineview", default=None, action="store_true", group=gui_grp + ) + + parser.unfinished_arguments["no-webengineview"] = dict( + long="no-webengineview", + default=None, + action="store_false", + dest="webengineview", help="Do not use HTML rendering.", - group=gui_grp) + group=gui_grp, + ) - if psyplot.__version__ < '1.0': + if psyplot.__version__ < "1.0": parser.set_main(start_app) parser.epilog += """ @@ -457,22 +534,28 @@ will execute ``print("Hello World")`` in the GUI. The output, of the `-s` and #: A boolean variable to check if the GUI is tested. This is set automatically #: true on CI services -UNIT_TESTING = os.getenv('CI') +UNIT_TESTING = os.getenv("CI") class ListGuiPluginsAction(argparse.Action): - - def __init__(self, option_strings, dest=argparse.SUPPRESS, nargs=None, - default=argparse.SUPPRESS, **kwargs): + def __init__( + self, + option_strings, + dest=argparse.SUPPRESS, + nargs=None, + default=argparse.SUPPRESS, + **kwargs, + ): if nargs is not None: raise ValueError("nargs not allowed") - kwargs['default'] = default + kwargs["default"] = default super(ListGuiPluginsAction, self).__init__( - option_strings, nargs=0, dest=dest, - **kwargs) + option_strings, nargs=0, dest=dest, **kwargs + ) def __call__(self, parser, namespace, values, option_string=None): import yaml + if not rcParams._plugins: list(rcParams._load_plugin_entrypoints()) print(yaml.dump(rcParams._plugins, default_flow_style=False)) @@ -493,17 +576,18 @@ def _set_opengl_implementation(option): except Exception: QQuickWindow = QSGRendererInterface = None from PyQt5.QtCore import QCoreApplication, Qt + if option is None: - option = rcParams['main.opengl'] - if option == 'software': + option = rcParams["main.opengl"] + if option == "software": QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL) if QQuickWindow is not None: QQuickWindow.setSceneGraphBackend(QSGRendererInterface.Software) - elif option == 'desktop': + elif option == "desktop": QCoreApplication.setAttribute(Qt.AA_UseDesktopOpenGL) if QQuickWindow is not None: QQuickWindow.setSceneGraphBackend(QSGRendererInterface.OpenGL) - elif option == 'gles': + elif option == "gles": QCoreApplication.setAttribute(Qt.AA_UseOpenGLES) if QQuickWindow is not None: QQuickWindow.setSceneGraphBackend(QSGRendererInterface.OpenGL) diff --git a/psyplot_gui/_version.py b/psyplot_gui/_version.py index 4834e231d78ad13581a5bdd9df73be221a7f3ac1..6942f8f49e1405abca15aafd87f967ba20597e72 100644 --- a/psyplot_gui/_version.py +++ b/psyplot_gui/_version.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag @@ -6,33 +9,7 @@ # that just contains the computed version number. # This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) -# -# Disclaimer -# ---------- -# -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. -# -# This file is originally released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) +# versioneer-0.21 (https://github.com/python-versioneer/python-versioneer) """Git implementation of _version.py.""" @@ -41,6 +18,7 @@ import os import re import subprocess import sys +from typing import Callable, Dict def get_keywords(): @@ -78,36 +56,42 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} -HANDLERS = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" + """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f + return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): +def run_command( + commands, args, cwd=None, verbose=False, hide_stderr=False, env=None +): """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + process = subprocess.Popen( + [command] + args, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr else None), + ) break - except EnvironmentError: + except OSError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue @@ -119,15 +103,13 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, if verbose: print("unable to find command, tried %s" % (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): @@ -139,19 +121,24 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + return { + "version": dirname[len(parentdir_prefix) :], + "full-revisionid": None, + "dirty": False, + "error": None, + "date": None, + } + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) + print( + "Tried directories %s but none started with prefix %s" + % (str(rootdirs), parentdir_prefix) + ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -164,22 +151,21 @@ def git_get_keywords(versionfile_abs): # _version.py. keywords = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @@ -187,10 +173,14 @@ def git_get_keywords(versionfile_abs): @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -203,11 +193,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -216,7 +206,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r"\d", r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -224,23 +214,35 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] + r = ref[len(tag_prefix) :] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r"\d", r): + continue if verbose: print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} + return { + "version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": None, + "date": date, + } # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} + return { + "version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": "no suitable tags", + "date": None, + } @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -248,11 +250,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): version string, meaning we're inside a checked out source tree. """ GITS = ["git"] + TAG_PREFIX_REGEX = "*" if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] + TAG_PREFIX_REGEX = r"\*" - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + _, rc = runner( + GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True + ) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -260,15 +265,24 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) + describe_out, rc = runner( + GITS, + [ + "describe", + "--tags", + "--dirty", + "--always", + "--long", + "--match", + "%s%s" % (tag_prefix, TAG_PREFIX_REGEX), + ], + cwd=root, + ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() @@ -278,6 +292,40 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner( + GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root + ) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -286,17 +334,18 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] + git_describe = git_describe[: git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + # unparsable. Maybe git-describe is misbehaving? + pieces["error"] = ( + "unable to parse git-describe output: '%s'" % describe_out + ) return pieces # tag @@ -305,10 +354,12 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) + pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( + full_tag, + tag_prefix, + ) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] + pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -319,13 +370,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) + count_out, rc = runner(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ + 0 + ].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces @@ -356,26 +410,77 @@ def render_pep440(pieces): rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces): + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver): + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces): + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post( + pieces["closest-tag"] + ) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % ( + post_version + 1, + pieces["distance"], + ) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] + rendered = "0.post0.dev%d" % pieces["distance"] return rendered @@ -406,12 +511,41 @@ def render_pep440_post(pieces): return rendered +def render_pep440_post_branch(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -471,21 +605,27 @@ def render_git_describe_long(pieces): def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} + return { + "version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None, + } if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": @@ -495,9 +635,13 @@ def render(pieces, style): else: raise ValueError("unknown style '%s'" % style) - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} + return { + "version": rendered, + "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], + "error": None, + "date": pieces.get("date"), + } def get_versions(): @@ -511,8 +655,9 @@ def get_versions(): verbose = cfg.verbose try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) + return git_versions_from_keywords( + get_keywords(), cfg.tag_prefix, verbose + ) except NotThisMethod: pass @@ -521,13 +666,16 @@ def get_versions(): # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): + for _ in cfg.versionfile_source.split("/"): root = os.path.dirname(root) except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None, + } try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) @@ -541,6 +689,10 @@ def get_versions(): except NotThisMethod: pass - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", + "date": None, + } diff --git a/psyplot_gui/backend.py b/psyplot_gui/backend.py index b02ae3a397ea3ac8cbafca4dd94cb60740d2fa98..c1c94ec90bf44ae34cabb0c0965439c905cea8b7 100644 --- a/psyplot_gui/backend.py +++ b/psyplot_gui/backend.py @@ -3,51 +3,45 @@ psyplot gui This backend is based upon matplotlibs qt4agg and qt5agg backends.""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only -from psyplot_gui.compat.qtcompat import ( - QDockWidget, Qt, QWidget, QVBoxLayout, with_qt5) -from psyplot_gui.common import DockMixin from matplotlib.backend_bases import FigureManagerBase from matplotlib.figure import Figure + +from psyplot_gui.common import DockMixin +from psyplot_gui.compat.qtcompat import ( + QDockWidget, + Qt, + QVBoxLayout, + QWidget, + with_qt5, +) + if with_qt5: from matplotlib.backends.backend_qt5agg import ( - show, FigureManagerQT, FigureCanvasQTAgg) + FigureCanvasQTAgg, + FigureManagerQT, + ) else: from matplotlib.backends.backend_qt4agg import ( - show, FigureManagerQT, FigureCanvasQTAgg) + FigureCanvasQTAgg, + FigureManagerQT, + ) class FiguresDock(QDockWidget): - """Reimplemented QDockWidget to remove the dock widget when closed - """ + """Reimplemented QDockWidget to remove the dock widget when closed""" def close(self, *args, **kwargs): """ Reimplemented to remove the dock widget from the mainwindow when closed """ from psyplot_gui.main import mainwindow + try: mainwindow.figures.remove(self) except Exception: @@ -69,7 +63,7 @@ def new_figure_manager(num, *args, **kwargs): """ Create a new figure manager instance """ - FigureClass = kwargs.pop('FigureClass', Figure) + FigureClass = kwargs.pop("FigureClass", Figure) thisFig = FigureClass(*args, **kwargs) return new_figure_manager_given_figure(num, thisFig) @@ -90,14 +84,18 @@ class PsyplotCanvasManager(FigureManagerQT): def __init__(self, canvas, num): from psyplot_gui.main import mainwindow + self.main = mainwindow if mainwindow is None: return super(PsyplotCanvasManager, self).__init__(canvas, num) parent_widget = FigureWidget() parent_widget.vbox = vbox = QVBoxLayout() self.window = dock = parent_widget.to_dock( - mainwindow, title="Figure %d" % num, position=Qt.TopDockWidgetArea, - docktype=None) + mainwindow, + title="Figure %d" % num, + position=Qt.TopDockWidgetArea, + docktype=None, + ) if mainwindow.figures: mainwindow.tabifyDockWidget(mainwindow.figures[-1], dock) mainwindow.figures.append(dock) @@ -106,7 +104,9 @@ class PsyplotCanvasManager(FigureManagerQT): self.window.setWindowTitle("Figure %d" % num) - self.toolbar = self._get_toolbar(canvas, parent_widget) + if hasattr(self, "_get_toolbar"): + # legacy solution for matplotlib < 3.6 + self.toolbar = self._get_toolbar(canvas, parent_widget) # add text label to status bar self.statusbar_label = mainwindow.figures_label @@ -136,6 +136,7 @@ class PsyplotCanvasManager(FigureManagerQT): # This will be called whenever the current axes is changed if self.toolbar is not None: self.toolbar.update() + self.canvas.figure.add_axobserver(notify_axes_change) def statusBar(self, *args, **kwargs): diff --git a/psyplot_gui/common.py b/psyplot_gui/common.py index d14517adbccb1effd43e52292f1b98a1ea245da4..7bb02ca15b76effb6c5acabe98008ad5d2b9f9da 100644 --- a/psyplot_gui/common.py +++ b/psyplot_gui/common.py @@ -1,38 +1,31 @@ """Common functions used for the psyplot gui""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only -import sys -import six import inspect +import logging +import os.path as osp +import sys import traceback as tb from functools import partial -import os.path as osp + +import six + from psyplot_gui.compat.qtcompat import ( - QDockWidget, QRegExpValidator, QtCore, QErrorMessage, QDesktopWidget, - QToolButton, QInputDialog, QIcon, QAction) -import logging + QAction, + QDesktopWidget, + QDockWidget, + QErrorMessage, + QIcon, + QInputDialog, + QRegExpValidator, + QtCore, + QToolButton, +) if six.PY2: try: @@ -48,6 +41,7 @@ def is_running_tests(): This function returns the :attr:`psyplot_gui.UNIT_TESTING` variable""" import psyplot_gui + return psyplot_gui.UNIT_TESTING @@ -59,7 +53,7 @@ def get_module_path(modname): def get_icon(name): """Get the path to an icon in the icons directory""" - return osp.join(get_module_path('psyplot_gui'), 'icons', name) + return osp.join(get_module_path("psyplot_gui"), "icons", name) class DockMixin(object): @@ -96,21 +90,23 @@ class DockMixin(object): @property def is_shown(self): """Boolean that is True, if the dock widget is shown""" - return (self.dock is not None and - self.dock.toggleViewAction().isChecked()) + return ( + self.dock is not None and self.dock.toggleViewAction().isChecked() + ) - def to_dock(self, main, title=None, position=None, docktype='pane', *args, - **kwargs): + def to_dock( + self, main, title=None, position=None, docktype="pane", *args, **kwargs + ): if title is None: title = self.title if title is None: - raise ValueError( - "No title specified for the %s widget" % (self)) + raise ValueError("No title specified for the %s widget" % (self)) if position is None: position = self.dock_position if position is None: - raise ValueError("No position specified for the %s widget (%s)" % ( - title, self)) + raise ValueError( + "No position specified for the %s widget (%s)" % (title, self) + ) self.title = title self.dock_position = position if self.dock is None: @@ -137,7 +133,6 @@ class DockMixin(object): The main window where the dock is added""" main.addDockWidget(self.dock_position, self.dock, *args, **kwargs) - def show_plugin(self): """Show the plugin widget""" a = self.dock.toggleViewAction() @@ -169,29 +164,32 @@ class DockMixin(object): group.addAction(action) return self._set_central_action - def create_view_action(self, main, docktype='pane'): + def create_view_action(self, main, docktype="pane"): if self._view_action is None: self._view_action = action = self.dock.toggleViewAction() - if docktype == 'pane': + if docktype == "pane": main.panes_menu.addAction(action) - elif docktype == 'df': + elif docktype == "df": main.dataframe_menu.addAction(action) return self._view_action def remove_plugin(self): """Remove this plugin and close it""" mainwindow = self.dock.parent() if self.dock else self.parent() - key = next((key for key, w in mainwindow.plugins.items() - if w is self), None) + key = next( + (key for key, w in mainwindow.plugins.items() if w is self), None + ) if mainwindow.centralWidget() is self: mainwindow.set_central_widget( - mainwindow.__class__.central_widget_key) + mainwindow.__class__.central_widget_key + ) if self._view_action is not None: mainwindow.panes_menu.removeAction(self._view_action) mainwindow.dataframe_menu.removeAction(self._view_action) if self._set_central_action is not None: mainwindow.central_widgets_menu.removeAction( - self._set_central_action) + self._set_central_action + ) if key is not None: del mainwindow.plugins[key] if self.dock is not None: @@ -209,15 +207,21 @@ class LoadFromConsoleButton(QToolButton): @property def instances2check_str(self): - return ', '.join('%s.%s' % (cls.__module__, cls.__name__) - for cls in self._instances2check) + return ", ".join( + "%s.%s" % (cls.__module__, cls.__name__) + for cls in self._instances2check + ) @property def potential_object_names(self): from ipykernel.inprocess.ipkernel import InProcessInteractiveShell + shell = InProcessInteractiveShell.instance() - return sorted(name for name, obj in shell.user_global_ns.items() - if not name.startswith('_') and self.check(obj)) + return sorted( + name + for name, obj in shell.user_global_ns.items() + if not name.startswith("_") and self.check(obj) + ) def __init__(self, instances=None, *args, **kwargs): """ @@ -227,46 +231,55 @@ class LoadFromConsoleButton(QToolButton): The classes that should be used for an instance check """ super(LoadFromConsoleButton, self).__init__(*args, **kwargs) - self.setIcon(QIcon(get_icon('console-go.png'))) + self.setIcon(QIcon(get_icon("console-go.png"))) if instances is not None and inspect.isclass(instances): - instances = (instances, ) + instances = (instances,) self._instances2check = instances self.error_msg = PyErrorMessage(self) self.clicked.connect(partial(self.get_from_shell, None)) def check(self, obj): - return True if not self._instances2check else isinstance( - obj, self._instances2check) + return ( + True + if not self._instances2check + else isinstance(obj, self._instances2check) + ) def get_from_shell(self, oname=None): """Open an input dialog, receive an object and emit the :attr:`object_loaded` signal""" if oname is None: oname, ok = QInputDialog.getItem( - self, 'Select variable', - 'Select a variable to import from the console', - self.potential_object_names) + self, + "Select variable", + "Select a variable to import from the console", + self.potential_object_names, + ) if not ok: return - if self.check(oname) and (self._instances2check or - not isinstance(oname, six.string_types)): + if self.check(oname) and ( + self._instances2check or not isinstance(oname, six.string_types) + ): obj = oname - oname = 'object' + oname = "object" else: found, obj = self.get_obj(oname.strip()) if found: if not self.check(obj): self.error_msg.showMessage( - 'Object must be an instance of %r, not %r' % ( + "Object must be an instance of %r, not %r" + % ( self.instances2check_str, - '%s.%s' % (type(obj).__module__, - type(obj).__name__))) + "%s.%s" + % (type(obj).__module__, type(obj).__name__), + ) + ) return else: if not oname.strip(): - msg = 'The variable name must not be empty!' + msg = "The variable name must not be empty!" else: - msg = 'Could not find object ' + oname + msg = "Could not find object " + oname self.error_msg.showMessage(msg) return self.object_loaded.emit(oname, obj) @@ -274,6 +287,7 @@ class LoadFromConsoleButton(QToolButton): def get_obj(self, oname): """Load an object from the current shell""" from psyplot_gui.main import mainwindow + return mainwindow.console.get_obj(oname) @@ -281,7 +295,7 @@ class ListValidator(QRegExpValidator): """A validator class to validate that a string consists of strings in a list of strings""" - def __init__(self, valid, sep=',', *args, **kwargs): + def __init__(self, valid, sep=",", *args, **kwargs): """ Parameters ---------- @@ -292,7 +306,7 @@ class ListValidator(QRegExpValidator): ``*args,**kwargs`` Determined by PyQt5.QtGui.QValidator """ - patt = QtCore.QRegExp('^((%s)(;;)?)+$' % '|'.join(valid)) + patt = QtCore.QRegExp("^((%s)(;;)?)+$" % "|".join(valid)) super(QRegExpValidator, self).__init__(patt, *args, **kwargs) @@ -301,18 +315,16 @@ class PyErrorMessage(QErrorMessage): method""" def showTraceback(self, header=None): - if is_running_tests(): raise s = io.StringIO() tb.print_exc(file=s) - last_tb = '<p>' + '<br>'.join(s.getvalue().splitlines()) + \ - '</p>' - header = header + '\n' if header else '' + last_tb = "<p>" + "<br>".join(s.getvalue().splitlines()) + "</p>" + header = header + "\n" if header else "" self.showMessage(header + last_tb) - available_width = QDesktopWidget().availableGeometry().width() / 3. - available_height = QDesktopWidget().availableGeometry().height() / 3. + available_width = QDesktopWidget().availableGeometry().width() / 3.0 + available_height = QDesktopWidget().availableGeometry().height() / 3.0 width = self.sizeHint().width() height = self.sizeHint().height() # The message window should cover at least one third of the screen @@ -321,12 +333,11 @@ class PyErrorMessage(QErrorMessage): def excepthook(self, type, value, traceback): s = io.StringIO() tb.print_exception(type, value, traceback, file=s) - last_tb = '<p>' + '<br>'.join(s.getvalue().splitlines()) + \ - '</p>' + last_tb = "<p>" + "<br>".join(s.getvalue().splitlines()) + "</p>" header = value.message if six.PY2 else str(value) - self.showMessage(header + '\n' + last_tb) - available_width = QDesktopWidget().availableGeometry().width() / 3. - available_height = QDesktopWidget().availableGeometry().height() / 3. + self.showMessage(header + "\n" + last_tb) + available_width = QDesktopWidget().availableGeometry().width() / 3.0 + available_height = QDesktopWidget().availableGeometry().height() / 3.0 width = self.sizeHint().width() height = self.sizeHint().height() # The message window should cover at least one third of the screen @@ -337,10 +348,11 @@ class StreamToLogger(object): """ Fake file-like stream object that redirects writes to a logger instance. """ + def __init__(self, logger, log_level=logging.INFO): self.logger = logger self.log_level = log_level - self.linebuf = '' + self.linebuf = "" def write(self, buf): for line in buf.rstrip().splitlines(): diff --git a/psyplot_gui/compat/__init__.py b/psyplot_gui/compat/__init__.py index 518d3484d89305daf2911a75a8a25b15fdc12b37..0439e7dbe24115104ae14fb510fe28299a2a31d4 100755 --- a/psyplot_gui/compat/__init__.py +++ b/psyplot_gui/compat/__init__.py @@ -1,24 +1,7 @@ """Compatibility module for psyplot-gui""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only diff --git a/psyplot_gui/compat/qtcompat.py b/psyplot_gui/compat/qtcompat.py index 5af7e4edc1f4485d9cef5b3fb487b321166af409..972c1b1f6ce601267f92df604e168bcedb38831e 100644 --- a/psyplot_gui/compat/qtcompat.py +++ b/psyplot_gui/compat/qtcompat.py @@ -1,78 +1,116 @@ """Compatibility module for the different versions of PyQt""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only + +import sys # make sure that the right pyqt version suitable for the IPython console is # loaded import six -import sys + from psyplot_gui.config.rcsetup import rcParams try: - from qtconsole.rich_jupyter_widget import RichJupyterWidget + from qtconsole.rich_jupyter_widget import RichJupyterWidget # noqa: F401 except ImportError: pass try: - import PyQt5 + import PyQt5 # noqa: F401 except ImportError: - from PyQt4.QtGui import ( - QMainWindow, QDockWidget, QToolBox, QApplication, QListWidget, - QListWidgetItem, QHBoxLayout, QVBoxLayout, QAbstractItemView, - QWidget, QPushButton, QFrame, QSplitter, QTreeWidget, QTreeWidgetItem, - QSizePolicy, QLabel, QLineEdit, QIcon, QToolButton, - QComboBox as OrigQComboBox, - QKeyEvent, QSortFilterProxyModel, QStandardItem, QStandardItemModel, - QCompleter, QStatusBar, QPlainTextEdit, QTextEdit, QToolBar, QMenu, - QAction, QTextCursor, QMessageBox, QCheckBox, QFileDialog, - QListView, QDesktopWidget, QValidator, QStyledItemDelegate, - QTableWidget, QTableWidgetItem, QRegExpValidator, QGridLayout, - QIntValidator, QErrorMessage, QInputDialog, QTabWidget, - QDoubleValidator, QGraphicsScene, QGraphicsRectItem, QGraphicsView, - QKeySequence, QStyleOptionViewItem, QDialog, QDialogButtonBox, - QStackedWidget, QScrollArea, QTableView, QHeaderView, QActionGroup) - from PyQt4 import QtCore - from PyQt4.QtCore import Qt - from PyQt4.QtWebKit import QWebView as QWebEngineView - from PyQt4.QtTest import QTest - from PyQt4 import QtGui - from PyQt4.Qt import PYQT_VERSION_STR as PYQT_VERSION - from PyQt4.Qt import QT_VERSION_STR as QT_VERSION + from PyQt4 import QtCore, QtGui # noqa: F401 + from PyQt4.Qt import PYQT_VERSION_STR as PYQT_VERSION # noqa: F401 + from PyQt4.Qt import QT_VERSION_STR as QT_VERSION # noqa: F401 + from PyQt4.QtCore import Qt # noqa: F401 + from PyQt4.QtGui import ( # noqa: F401 + QAbstractItemView, + QAction, + QActionGroup, + QApplication, + QCheckBox, + ) + from PyQt4.QtGui import QComboBox as OrigQComboBox + from PyQt4.QtGui import ( # noqa: F401 + QCompleter, + QDesktopWidget, + QDialog, + QDialogButtonBox, + QDockWidget, + QDoubleValidator, + QErrorMessage, + QFileDialog, + QFrame, + QGraphicsRectItem, + QGraphicsScene, + QGraphicsView, + QGridLayout, + QHBoxLayout, + QHeaderView, + QIcon, + QInputDialog, + QIntValidator, + QKeyEvent, + QKeySequence, + QLabel, + QLineEdit, + QListView, + QListWidget, + QListWidgetItem, + QMainWindow, + QMenu, + QMessageBox, + QPlainTextEdit, + QPushButton, + QRegExpValidator, + QScrollArea, + QSizePolicy, + QSortFilterProxyModel, + QSplitter, + QStackedWidget, + QStandardItem, + QStandardItemModel, + QStatusBar, + QStyledItemDelegate, + QStyleOptionViewItem, + QTableView, + QTableWidget, + QTableWidgetItem, + QTabWidget, + QTextCursor, + QTextEdit, + QToolBar, + QToolBox, + QToolButton, + QTreeWidget, + QTreeWidgetItem, + QValidator, + QVBoxLayout, + QWidget, + ) + from PyQt4.QtTest import QTest # noqa: F401 + from PyQt4.QtWebKit import QWebView as QWebEngineView # noqa: F401 + with_qt5 = False QSignalSpy = None try: - from PyQt4.QtCore import QString, QByteArray + from PyQt4.QtCore import QByteArray, QString except ImportError: + def isstring(s): return isinstance(s, six.string_types) + else: + def isstring(s): return isinstance( - s, tuple(list(six.string_types) + [QString, QByteArray])) + s, tuple(list(six.string_types) + [QString, QByteArray]) + ) class QComboBox(OrigQComboBox): - currentTextChanged = QtCore.pyqtSignal(str) def __init__(self, *args, **kwargs): @@ -90,35 +128,87 @@ except ImportError: self.setCurrentIndex(idx) else: - from PyQt5.QtWidgets import ( - QMainWindow, QDockWidget, QToolBox, QApplication, QListWidget, - QListWidgetItem, QHBoxLayout, QVBoxLayout, QAbstractItemView, - QWidget, QPushButton, QFrame, QSplitter, QTreeWidget, QTreeWidgetItem, - QSizePolicy, QLabel, QLineEdit, QToolButton, QComboBox, QCompleter, - QStatusBar, QPlainTextEdit, QTextEdit, QToolBar, QMenu, - QAction, QMessageBox, QCheckBox, QFileDialog, QListView, - QDesktopWidget, QStyledItemDelegate, QTableWidget, QTableWidgetItem, - QGridLayout, QErrorMessage, QInputDialog, QTabWidget, - QGraphicsScene, QGraphicsRectItem, QGraphicsView, QStyleOptionViewItem, - QDialog, QDialogButtonBox, QStackedWidget, QScrollArea, - QTableView, QHeaderView, QActionGroup) - from PyQt5.QtGui import ( - QIcon, QKeyEvent, QStandardItem, QStandardItemModel, QTextCursor, - QValidator, QRegExpValidator, QIntValidator, QDoubleValidator, - QKeySequence) from PyQt5 import QtCore - from PyQt5.QtCore import Qt, QSortFilterProxyModel - if rcParams['help_explorer.use_webengineview']: + from PyQt5.QtCore import QSortFilterProxyModel, Qt # noqa: F401 + from PyQt5.QtGui import ( # noqa: F401 + QDoubleValidator, + QIcon, + QIntValidator, + QKeyEvent, + QKeySequence, + QRegExpValidator, + QStandardItem, + QStandardItemModel, + QTextCursor, + QValidator, + ) + from PyQt5.QtWidgets import ( # noqa: F401 + QAbstractItemView, + QAction, + QActionGroup, + QApplication, + QCheckBox, + QComboBox, + QCompleter, + QDesktopWidget, + QDialog, + QDialogButtonBox, + QDockWidget, + QErrorMessage, + QFileDialog, + QFrame, + QGraphicsRectItem, + QGraphicsScene, + QGraphicsView, + QGridLayout, + QHBoxLayout, + QHeaderView, + QInputDialog, + QLabel, + QLineEdit, + QListView, + QListWidget, + QListWidgetItem, + QMainWindow, + QMenu, + QMessageBox, + QPlainTextEdit, + QPushButton, + QScrollArea, + QSizePolicy, + QSplitter, + QStackedWidget, + QStatusBar, + QStyledItemDelegate, + QStyleOptionViewItem, + QTableView, + QTableWidget, + QTableWidgetItem, + QTabWidget, + QTextEdit, + QToolBar, + QToolBox, + QToolButton, + QTreeWidget, + QTreeWidgetItem, + QVBoxLayout, + QWidget, + ) + + if rcParams["help_explorer.use_webengineview"]: try: - from PyQt5.QtWebEngineWidgets import QWebEngineView + from PyQt5.QtWebEngineWidgets import QWebEngineView # noqa: F401 except ImportError: - from PyQt5.QtWebKitWidgets import QWebView as QWebEngineView + from PyQt5.QtWebKitWidgets import ( # noqa: F401 + QWebView as QWebEngineView, + ) else: QWebEngineView = None - from PyQt5.QtTest import QTest, QSignalSpy - from PyQt5 import QtGui - from PyQt5.Qt import PYQT_VERSION_STR as PYQT_VERSION - from PyQt5.Qt import QT_VERSION_STR as QT_VERSION + from PyQt5 import QtGui # noqa: F401 + from PyQt5.Qt import PYQT_VERSION_STR as PYQT_VERSION # noqa: F401 + from PyQt5.Qt import QT_VERSION_STR as QT_VERSION # noqa: F401 + from PyQt5.QtTest import QSignalSpy, QTest # noqa: F401 + with_qt5 = True def isstring(s): @@ -129,7 +219,7 @@ def asstring(s): return six.text_type(s) -if sys.platform == 'darwin': +if sys.platform == "darwin": # make sure to register the open file event OrigQApplication = QApplication @@ -139,9 +229,12 @@ if sys.platform == 'darwin': def event(self, event): from psyplot_gui.config.rcsetup import rcParams - if (rcParams['main.listen_to_port'] and - event.type() == QtCore.QEvent.FileOpen): + if ( + rcParams["main.listen_to_port"] + and event.type() == QtCore.QEvent.FileOpen + ): from psyplot_gui.main import mainwindow + if mainwindow is not None: opened = mainwindow.open_files([event.file()]) if opened: diff --git a/psyplot_gui/config/__init__.py b/psyplot_gui/config/__init__.py index b0e068c30f0586c6dd6f1c1927360c3d84fb1ce4..042e35ad8e54b05a90395136df0ca599a4057985 100755 --- a/psyplot_gui/config/__init__.py +++ b/psyplot_gui/config/__init__.py @@ -5,41 +5,25 @@ Default parameters are defined in the :data:`rcsetup.defaultParams` dictionary, however you can set up your own configuration in a yaml file (see :func:`psyplot.load_rc_from_file`)""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only import os.path as osp + from psyplot.config.logsetup import setup_logging from psyplot.config.rcsetup import psyplot_fname #: :class:`str`. Path to the yaml logging configuration file logcfg_path = setup_logging( - default_path=osp.join(osp.dirname(__file__), 'logging.yml'), - env_key='LOG_PSYPLOTGUI') + default_path=osp.join(osp.dirname(__file__), "logging.yml"), + env_key="LOG_PSYPLOTGUI", +) #: class:`str` or ``None``. Path to the yaml configuration file (if found). #: See :func:`psyplot.config.rcsetup.psyplot_fname` for further #: information -config_path = psyplot_fname(env_key='PSYPLOTGUIRC', - fname='psyplotguirc.yaml') +config_path = psyplot_fname(env_key="PSYPLOTGUIRC", fname="psyplotguirc.yaml") diff --git a/psyplot_gui/config/logging.yml b/psyplot_gui/config/logging.yml index dd94e5346d441797388de71d1b4a5373e95570b8..3d1c6e0bbae7e088d406598679e6fb356794a491 100755 --- a/psyplot_gui/config/logging.yml +++ b/psyplot_gui/config/logging.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC-BY-4.0 + --- # logging settings for the nc2map module @@ -72,4 +76,4 @@ loggers: level: INFO -... \ No newline at end of file +... diff --git a/psyplot_gui/config/logging_debug.yml b/psyplot_gui/config/logging_debug.yml index 5832c277f6e61368f575e352a5a456246484d300..d64465462ecc78b4a6ed892f3377d3261250510b 100755 --- a/psyplot_gui/config/logging_debug.yml +++ b/psyplot_gui/config/logging_debug.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC-BY-4.0 + --- # debug logging settings (sets the level of the nc2map logger to DEBUG) @@ -71,4 +75,4 @@ loggers: propagate: False level: DEBUG -... \ No newline at end of file +... diff --git a/psyplot_gui/config/rcsetup.py b/psyplot_gui/config/rcsetup.py index 786405ab7bdaa48381942a8de84769856ffa47ff..ed40cd1d8e33f8491b5d82c42ba83d4d400da423 100755 --- a/psyplot_gui/config/rcsetup.py +++ b/psyplot_gui/config/rcsetup.py @@ -2,34 +2,22 @@ This module defines the necessary configuration parts for the psyplot gui""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only -import six import logging + +import six +from matplotlib.rcsetup import validate_bool, validate_int from psyplot.config.rcsetup import ( - RcParams, psyplot_fname, validate_bool_maybe_none, validate_stringlist) -from matplotlib.rcsetup import validate_int, validate_bool + RcParams, + psyplot_fname, + validate_bool_maybe_none, + validate_stringlist, +) def try_and_error(*funcs): @@ -43,6 +31,7 @@ def try_and_error(*funcs): Returns ------- function""" + def validate(value): exc = None for func in funcs: @@ -51,6 +40,7 @@ def try_and_error(*funcs): except (ValueError, TypeError) as e: exc = e raise exc + return validate @@ -95,7 +85,7 @@ def validate_none(b): ValueError""" if isinstance(b, six.string_types): b = b.lower() - if b is None or b == 'none': + if b is None or b == "none": return None else: raise ValueError('Could not convert "%s" to None' % b) @@ -103,7 +93,7 @@ def validate_none(b): def validate_all(v): """Test if ``v == 'all'``""" - if v != 'all': + if v != "all": raise ValueError("The value must be 'all'") return six.text_type(v) @@ -112,8 +102,8 @@ class GuiRcParams(RcParams): """RcParams for the psyplot-gui package.""" HEADER = RcParams.HEADER.replace( - 'psyplotrc.yml', 'psyplotguirc.yml').replace( - 'PSYPLOTRC', 'psyplotrc.yml') + "psyplotrc.yml", "psyplotguirc.yml" + ).replace("PSYPLOTRC", "psyplotrc.yml") def load_from_file(self, fname=None): """ @@ -131,8 +121,9 @@ class GuiRcParams(RcParams): See Also -------- dump_to_file, psyplot_fname""" - fname = fname or psyplot_fname(env_key='PSYPLOTGUIRC', - fname='psyplotguirc.yml') + fname = fname or psyplot_fname( + env_key="PSYPLOTGUIRC", fname="psyplotguirc.yml" + ) if fname: super(GuiRcParams, self).load_from_file(fname) @@ -144,31 +135,44 @@ class GuiRcParams(RcParams): pkg_resources.EntryPoint The entry point for the psyplot plugin module""" from pkg_resources import iter_entry_points - inc = self['plugins.include'] - exc = self['plugins.exclude'] + + inc = self["plugins.include"] + exc = self["plugins.exclude"] logger = logging.getLogger(__name__) self._plugins = self._plugins or [] - for ep in iter_entry_points('psyplot_gui'): - plugin_name = '%s:%s:%s' % (ep.module_name, ':'.join(ep.attrs), - ep.name) + for ep in iter_entry_points("psyplot_gui"): + try: + ep.module + except AttributeError: # python<3.10 + try: + ep.module = ep.pattern.match(ep.value).group("module") + except AttributeError: # python<3.8 + ep.module = ep.module_name + + plugin_name = "%s:%s:%s" % (ep.module, ":".join(ep.attrs), ep.name) # check if the user wants to explicitly this plugin include_user = None if inc: include_user = ( - ep.module_name in inc or ep.name in inc or - '%s:%s' % (ep.module_name, ':'.join(ep.attrs)) in inc) - if include_user is None and exc == 'all': + ep.module in inc + or ep.name in inc + or "%s:%s" % (ep.module, ":".join(ep.attrs)) in inc + ) + if include_user is None and exc == "all": include_user = False elif include_user is None: # check for exclude include_user = not ( - ep.module_name in exc or ep.name in exc or - '%s:%s' % (ep.module_name, ':'.join(ep.attrs)) in exc) + ep.module in exc + or ep.name in exc + or "%s:%s" % (ep.module, ":".join(ep.attrs)) in exc + ) if not include_user: - logger.debug('Skipping plugin %s: Excluded by user', - plugin_name) + logger.debug( + "Skipping plugin %s: Excluded by user", plugin_name + ) else: - logger.debug('Loading plugin %s', plugin_name) + logger.debug("Loading plugin %s", plugin_name) self._plugins.append(str(ep)) yield ep @@ -185,87 +189,121 @@ class GuiRcParams(RcParams): ----- ``*args`` and ``**kwargs`` are ignored """ + def format_ep(ep): - return '%s:%s:%s' % (ep.module_name, ':'.join(ep.attrs), ep.name) + return "%s:%s:%s" % (ep.module_name, ":".join(ep.attrs), ep.name) + return { - format_ep(ep): ep.load() for ep in self._load_plugin_entrypoints()} + format_ep(ep): ep.load() for ep in self._load_plugin_entrypoints() + } #: :class:`dict` with default values and validation functions defaultParams = { - # gui settings - 'backend': [ - 'psyplot', + "backend": [ + "psyplot", try_and_error(validate_str, validate_none), - 'Backend to use when using the graphical user interface. The current ' - 'backend is used and no changes are made. Note that it is usually not ' - 'possible to change the backend after importing the psyplot.project ' - 'module. The default backend embeds the figures into the '], - 'help_explorer.use_webengineview': [ - True, validate_bool, + "Backend to use when using the graphical user interface. The current " + "backend is used and no changes are made. Note that it is usually not " + "possible to change the backend after importing the psyplot.project " + "module. The default backend embeds the figures into the ", + ], + "help_explorer.use_webengineview": [ + True, + validate_bool, "Enable the PyQt5.QtWebEngineWidgets.QWebEngineView which might not " - "work under certain circumstances."], - 'help_explorer.use_intersphinx': [ - None, validate_bool_maybe_none, - 'Use the intersphinx extension and link to the online documentations ' - 'of matplotlib, pyplot, psyplot, numpy, etc. when converting rst ' - 'docstrings. The inventories are loaded when the first object is ' - 'documented. If None, intersphinx is only used when ' - '`help_explorer.online` is True and you are not using windows'], - 'help_explorer.render_docs_parallel': [ - True, validate_bool, - 'Boolean whether the html docs are rendered in a separate process'], - 'help_explorer.online': [ - None, validate_bool_maybe_none, - 'Switch that controls whether the online functions of the help ' - 'explorer shall be enabled. False implies that ' - 'help_explorer.use_intersphinx is set to False'], - 'console.start_channels': [ - True, validate_bool, - 'Start the different channels of the KernelClient'], - 'console.connect_to_help': [ - True, validate_bool, - 'Whether the console shall be connected to the help_explorer or not'], - 'console.auto_set_mp': [ - True, validate_bool, + "work under certain circumstances.", + ], + "help_explorer.use_intersphinx": [ + None, + validate_bool_maybe_none, + "Use the intersphinx extension and link to the online documentations " + "of matplotlib, pyplot, psyplot, numpy, etc. when converting rst " + "docstrings. The inventories are loaded when the first object is " + "documented. If None, intersphinx is only used when " + "`help_explorer.online` is True and you are not using windows", + ], + "help_explorer.render_docs_parallel": [ + True, + validate_bool, + "Boolean whether the html docs are rendered in a separate process", + ], + "help_explorer.online": [ + None, + validate_bool_maybe_none, + "Switch that controls whether the online functions of the help " + "explorer shall be enabled. False implies that " + "help_explorer.use_intersphinx is set to False", + ], + "console.start_channels": [ + True, + validate_bool, + "Start the different channels of the KernelClient", + ], + "console.connect_to_help": [ + True, + validate_bool, + "Whether the console shall be connected to the help_explorer or not", + ], + "console.auto_set_mp": [ + True, + validate_bool, "If True, then the 'mp' variable in the console is automatically set " - "when the current main project changes"], - 'console.auto_set_sp': [ - True, validate_bool, + "when the current main project changes", + ], + "console.auto_set_sp": [ + True, + validate_bool, "If True, then the 'sp' variable in the console is automatically set " - "when the current sub project changes"], - 'main.open_files_port': [ - 30124, validate_int, "The port number used when new files are opened"], - 'main.listen_to_port': [ - True, validate_bool, + "when the current sub project changes", + ], + "main.open_files_port": [ + 30124, + validate_int, + "The port number used when new files are opened", + ], + "main.listen_to_port": [ + True, + validate_bool, "If True and the psyplot gui is already running, new files are opened " - "in that gui"], - 'main.opengl': [ - 'software', validate_str, + "in that gui", + ], + "main.opengl": [ + "software", + validate_str, "The opengl implementation to use. Should be one of 'software', " - "'desktop', 'gles' or 'automatic'."], - 'content.load_tooltips': [ - True, validate_bool, + "'desktop', 'gles' or 'automatic'.", + ], + "content.load_tooltips": [ + True, + validate_bool, "If True, a lazy load is performed on the arrays and data sets and " "their string representation is displayed as tool tip. This part of " "the data into memory. It is recommended to set this to False for " - "remote data."], - 'fmt.sort_by_key': [ - True, validate_bool, + "remote data.", + ], + "fmt.sort_by_key": [ + True, + validate_bool, "If True, the formatoptions in the Formatoptions widget are sorted by " - "their formatoption key rather than by their name."], - 'plugins.include': [ - None, try_and_error(validate_none, validate_stringlist), + "their formatoption key rather than by their name.", + ], + "plugins.include": [ + None, + try_and_error(validate_none, validate_stringlist), "The plugins to load. Can be either None to load all that are not " "explicitly excluded by the 'plugins.exclude' key or a list of " "plugins to include. List items can be either module names, plugin " - "names or the module name and widget via '<module_name>:<widget>'"], - 'plugins.exclude': [ - [], try_and_error(validate_all, validate_stringlist), + "names or the module name and widget via '<module_name>:<widget>'", + ], + "plugins.exclude": [ + [], + try_and_error(validate_all, validate_stringlist), "The plugins to exclude from loading. Can be either 'all' to exclude " - "all plugins or a list like in 'plugins.include'."], - } + "all plugins or a list like in 'plugins.include'.", + ], +} #: :class:`~psyplot.config.rcsetup.RcParams` instance that stores default #: formatoptions and configuration settings. diff --git a/psyplot_gui/console.py b/psyplot_gui/console.py index fa6b8c1b19522086277912dbba0408bfbbfacc0c..89e00d176308566ceaec790b188eade817c327d2 100644 --- a/psyplot_gui/console.py +++ b/psyplot_gui/console.py @@ -6,54 +6,44 @@ Based on the earlier example in the IPython repository, this has been updated to use qtconsole. """ -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only +import logging import re import sys -try: - from qtconsole.inprocess import QtInProcessRichJupyterWidget -except ImportError: - from qtconsole.rich_jupyter_widget import ( - RichJupyterWidget as QtInProcessRichJupyterWidget) - import ipykernel +import psyplot +import psyplot.project as psy +from psyplot.docstring import docstrings +from qtconsole.inprocess import QtInProcessKernelManager from tornado import ioloop from zmq.eventloop import ioloop as zmq_ioloop -from qtconsole.inprocess import QtInProcessKernelManager -from psyplot_gui.compat.qtcompat import ( - with_qt5, QtCore, Qt, QTextEdit, QTextCursor, QKeySequence, asstring) -from psyplot_gui.common import StreamToLogger -import psyplot + import psyplot_gui from psyplot_gui import rcParams -from psyplot_gui.common import DockMixin -import psyplot.project as psy -from psyplot.docstring import docstrings +from psyplot_gui.common import DockMixin, StreamToLogger +from psyplot_gui.compat.qtcompat import ( + QKeySequence, + Qt, + QtCore, + QTextCursor, + QTextEdit, + asstring, + with_qt5, +) +try: + from qtconsole.inprocess import QtInProcessRichJupyterWidget +except ImportError: + from qtconsole.rich_jupyter_widget import ( + RichJupyterWidget as QtInProcessRichJupyterWidget, + ) -import logging #: HACK: Boolean that is True if the prompt should be used. This unfortunately #: is necessary for qtconsole >= 4.3 when running the tests @@ -61,10 +51,11 @@ _with_prompt = True modules2import = [ - ('psyplot.project', 'psy'), - ('xarray', 'xr'), - ('pandas', 'pd'), - ('numpy', 'np')] + ("psyplot.project", "psy"), + ("xarray", "xr"), + ("pandas", "pd"), + ("numpy", "np"), +] symbols_patt = re.compile(r"[^\'\"a-zA-Z0-9_.]") @@ -85,6 +76,7 @@ def init_asyncio_patch(): """ if sys.platform.startswith("win") and sys.version_info >= (3, 8): import asyncio + try: from asyncio import ( WindowsProactorEventLoopPolicy, @@ -94,7 +86,10 @@ def init_asyncio_patch(): pass # not affected else: - if type(asyncio.get_event_loop_policy()) is WindowsProactorEventLoopPolicy: + if ( + type(asyncio.get_event_loop_policy()) + is WindowsProactorEventLoopPolicy + ): # WindowsProactorEventLoopPolicy is not compatible with tornado 6 # fallback to the pre-3.8 default of Selector asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) @@ -121,12 +116,11 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): dock_position = Qt.RightDockWidgetArea - title = 'Console' + title = "Console" - rc = rcParams.find_and_replace( - 'console.', pattern_base='console\.') + rc = rcParams.find_and_replace("console.", pattern_base=r"console\.") - intro_msg = '' + intro_msg = "" run_script = QtCore.pyqtSignal(list) @@ -161,26 +155,29 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): if sys.stderr is None: sys.stderr = StreamToLogger(logger) kernel_manager.start_kernel(show_banner=False) - if ipykernel.__version__ < '5.1.1': + if ipykernel.__version__ < "5.1.1": # monkey patch to fix # https://github.com/ipython/ipykernel/issues/370 def _abort_queues(kernel): pass + kernel_manager.kernel._abort_queues = _abort_queues sys.stdout = orig_stdout sys.stderr = orig_stderr kernel = kernel_manager.kernel - kernel.gui = 'qt4' if not with_qt5 else 'qt' + kernel.gui = "qt4" if not with_qt5 else "qt" kernel_client = kernel_manager.client() - if rcParams['console.start_channels']: + if rcParams["console.start_channels"]: kernel_client.start_channels() - self.help_explorer = kwargs.pop('help_explorer', None) + self.help_explorer = kwargs.pop("help_explorer", None) super(ConsoleWidget, self).__init__(*args, parent=main, **kwargs) - self.intro_msg = docstrings.dedent(""" + self.intro_msg = ( + docstrings.dedent( + """ psyplot version: %s gui version: %s @@ -196,19 +193,25 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): variables in the console are adjusted. To disable this behaviour, set:: >>> import psyplot_gui - >>> psyplot_gui.rcParams['console.auto_set_mp'] = False - >>> psyplot_gui.rcParams['console.auto_set_sp'] = False + >>> psyplot_gui.rcParams["console.auto_set_mp"] = False + >>> psyplot_gui.rcParams["console.auto_set_sp"] = False To inspect and object in the console and display it's documentation in - the help explorer, type 'Ctrl + I' or a '?' after the object""") % ( - psyplot.__version__, psyplot_gui.__version__, - '\n - '.join('%s as %s' % t for t in modules2import)) + the help explorer, type 'Ctrl + I' or a '?' after the object""" + ) + % ( + psyplot.__version__, + psyplot_gui.__version__, + "\n - ".join("%s as %s" % t for t in modules2import), + ) + ) self.kernel_manager = kernel_manager self.kernel_client = kernel_client self.run_command_in_shell( - '\n'.join('import %s as %s' % t for t in modules2import)) + "\n".join("import %s as %s" % t for t in modules2import) + ) self.exit_requested.connect(self._close_mainwindow) self.exit_requested.connect(QtCore.QCoreApplication.instance().quit) @@ -216,8 +219,9 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): # reserved for mainwindows save action try: main.register_shortcut( - self.export_action, QKeySequence( - 'Ctrl+Alt+S', QKeySequence.NativeText)) + self.export_action, + QKeySequence("Ctrl+Alt+S", QKeySequence.NativeText), + ) except AttributeError: pass @@ -239,19 +243,19 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): def update_mp(self, project): """Update the `mp` variable in the shell is ``rcParams['console.auto_set_mp']`` with a main project""" - if self.rc['auto_set_mp'] and project is not None and project.is_main: - self.run_command_in_shell('mp = psy.gcp(True)') + if self.rc["auto_set_mp"] and project is not None and project.is_main: + self.run_command_in_shell("mp = psy.gcp(True)") def update_sp(self, project): """Update the `sp` variable in the shell is ``rcParams['console.auto_set_sp']`` with a sub project""" - if self.rc['auto_set_sp'] and (project is None or not project.is_main): - self.run_command_in_shell('sp = psy.gcp()') + if self.rc["auto_set_sp"] and (project is None or not project.is_main): + self.run_command_in_shell("sp = psy.gcp()") def show_current_help(self, to_end=False, force=False): """Show the help of the object at the cursor position if ``rcParams['console.connect_to_help']`` is set""" - if not force and not self.rc['connect_to_help']: + if not force and not self.rc["connect_to_help"]: return obj_text = self.get_current_object(to_end) if obj_text is not None and self.help_explorer is not None: @@ -275,8 +279,7 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): True, if the object could be found object or None The requested object or None if it could not be found""" - info = self.kernel_manager.kernel.shell._object_find( - obj_text) + info = self.kernel_manager.kernel.shell._object_find(obj_text) if info.found: return True, info.obj else: @@ -295,7 +298,7 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): curr = cursor.position() start = curr - cursor.positionInBlock() txt = c.toPlainText()[start:curr] - eol = '' + eol = "" if to_end: cursor.movePosition(QTextCursor.EndOfBlock) end = cursor.position() @@ -303,12 +306,12 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): eol = c.toPlainText()[curr:end] m = symbols_patt.search(eol) if m: - eol = eol[:m.start()] + eol = eol[: m.start()] if not txt: return txt txt = asstring(txt) - txt = txt.rsplit('\n', 1)[-1] + txt = txt.rsplit("\n", 1)[-1] txt_end = "" for startchar, endchar in ["[]", "()"]: if txt.endswith(endchar): @@ -321,9 +324,9 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): try: while token is None or symbols_patt.match(token): token = tokens.pop() - if token.endswith('.'): + if token.endswith("."): token = token[:-1] - if token.startswith('.'): + if token.startswith("."): # Invalid object name return None token += txt_end @@ -337,7 +340,7 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): def run_script_in_shell(self, script): """Run a script in the shell""" - self.kernel_manager.kernel.shell.run_line_magic('run', script) + self.kernel_manager.kernel.shell.run_line_magic("run", script) def _run_command_in_shell(self, args): # 0: filenames @@ -349,10 +352,12 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): """Run a script in the shell""" ret = self.kernel_manager.kernel.shell.run_code(code, *args, **kwargs) import IPython - if IPython.__version__ < '7.0': # run_code is an asyncio.coroutine + + if IPython.__version__ < "7.0": # run_code is an asyncio.coroutine return ret else: import asyncio + gathered = asyncio.gather(ret) loop = asyncio.get_event_loop() ret = loop.run_until_complete(gathered) @@ -360,6 +365,7 @@ class ConsoleWidget(QtInProcessRichJupyterWidget, DockMixin): def _close_mainwindow(self): from psyplot_gui.main import mainwindow + if mainwindow is not None: mainwindow.close() else: diff --git a/psyplot_gui/content_widget.py b/psyplot_gui/content_widget.py index ab43f01e5fc00fd9171b5694184e72ee138e4182..32d93ffe6857e91a61c970ece34878dca091dc7c 100644 --- a/psyplot_gui/content_widget.py +++ b/psyplot_gui/content_widget.py @@ -7,54 +7,46 @@ There is no need to import this module because the :class:`GuiProject` class defined here replaces the project class in the :mod:`psyplot.project` module.""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only -import sys -import six import os.path as osp import re -import sip +import sys import weakref from itertools import chain -from psyplot_gui import rcParams -from psyplot_gui.compat.qtcompat import ( - QToolBox, QListWidget, QListWidgetItem, QAbstractItemView, - QWidget, QPushButton, QHBoxLayout, QVBoxLayout, QTreeWidget, - QTreeWidgetItem, QtCore, QMenu, QAction, Qt, QLabel, QScrollArea) +from xml.sax.saxutils import escape + +import sip +import six from psyplot.config.rcsetup import safe_list -from psyplot.compat.pycompat import OrderedDict, map, range -from psyplot.project import scp, gcp, Project from psyplot.data import ArrayList, InteractiveList +from psyplot.project import Project, gcp, scp from psyplot.utils import _TempBool -from psyplot_gui.common import DockMixin -from xml.sax.saxutils import escape - -html_escape_table = { - '"': """, - "'": "'" - } +from psyplot_gui import rcParams +from psyplot_gui.common import DockMixin +from psyplot_gui.compat.qtcompat import ( + QAbstractItemView, + QAction, + QHBoxLayout, + QListWidget, + QListWidgetItem, + QMenu, + QPushButton, + Qt, + QtCore, + QToolBox, + QTreeWidget, + QTreeWidgetItem, + QVBoxLayout, + QWidget, +) + +html_escape_table = {'"': """, "'": "'"} def escape_html(s): @@ -90,7 +82,7 @@ class ArrayItem(QListWidgetItem): """ if not sip.isdeleted(self): self.setText(self.arr()._short_info()) - if rcParams['content.load_tooltips']: + if rcParams["content.load_tooltips"]: if isinstance(self.arr(), InteractiveList): self.setToolTip(str(self.arr())) else: @@ -126,15 +118,19 @@ class PlotterList(QListWidget): @property def arrays(self): """List of The InteractiveBase instances in this list""" - return ArrayList([ - getattr(item.arr(), 'arr', item.arr()) - for item in self.array_items]) + return ArrayList( + [ + getattr(item.arr(), "arr", item.arr()) + for item in self.array_items + ] + ) @property def array_items(self): """Iterable of :class:`ArrayItem` items in this list""" - return filter(lambda i: i is not None, - map(self.item, range(self.count()))) + return filter( + lambda i: i is not None, map(self.item, range(self.count())) + ) def __init__(self, plotter_type=None, *args, **kwargs): """ @@ -211,8 +207,7 @@ class PlotterList(QListWidget): self.insertItem(0, self.takeItem(i)) cp = gcp() for item in self.array_items: - item.setSelected( - getattr(item.arr(), 'arr', item.arr()) in cp) + item.setSelected(getattr(item.arr(), "arr", item.arr()) in cp) self.updated_from_project.emit(self) def update_cp(self, *args, **kwargs): @@ -223,7 +218,8 @@ class PlotterList(QListWidget): selected = [item.arr().arr_name for item in self.selectedItems()] arrays = self.arrays other_selected = [ - arr.psy.arr_name for arr in sp if arr not in arrays] + arr.psy.arr_name for arr in sp if arr not in arrays + ] with self._no_project_update: scp(mp(arr_name=selected + other_selected)) @@ -241,9 +237,9 @@ class ProjectContent(QToolBox): This toolbox contains several :class:`PlotterList` that show the content of the current main and subproject""" - #: :class:`OrderedDict` containing the :class:`PlotterList` instances + #: :class:`dict` containing the :class:`PlotterList` instances #: of the different selection attributes - lists = OrderedDict() + lists = dict() @property def current_names(self): @@ -251,9 +247,9 @@ class ProjectContent(QToolBox): def __init__(self, *args, **kwargs): super(ProjectContent, self).__init__(*args, **kwargs) - self.lists = OrderedDict() - for attr in chain(['All'], sorted(Project._registered_plotters)): - item = self.add_plotterlist(attr, force=(attr == 'All')) + self.lists = dict() + for attr in chain(["All"], sorted(Project._registered_plotters)): + item = self.add_plotterlist(attr, force=(attr == "All")) self.lists[attr] = item self.currentChanged.connect(self.update_current_list) Project.oncpchange.connect(self.update_lists) @@ -267,7 +263,7 @@ class ProjectContent(QToolBox): def add_plotterlist(self, identifier, force=False): """Create a :class:`PlotterList` from an identifier from the :class:`psyplot.project.Project` class""" - attr = identifier if identifier != 'All' else None + attr = identifier if identifier != "All" else None item = PlotterList(attr) if not item.can_import_plotter: return item @@ -293,7 +289,7 @@ class ProjectContent(QToolBox): l.update_from_project(p) if l.is_empty: l.disconnect_items() - if name != 'All' and l.is_empty: + if name != "All" and l.is_empty: i = self.indexOf(l) self.removeItem(i) elif not l.is_empty and name not in current_items: @@ -306,7 +302,8 @@ class SelectAllButton(QPushButton): def __init__(self, *args, **kwargs): super(SelectAllButton, self).__init__(*args, **kwargs) self.setToolTip( - 'Click to select all data arrays in the entire project') + "Click to select all data arrays in the entire project" + ) self.clicked.connect(self.select_all) Project.oncpchange.connect(self.enable_from_project) @@ -324,7 +321,7 @@ class SelectNoneButton(QPushButton): def __init__(self, *args, **kwargs): super(SelectNoneButton, self).__init__(*args, **kwargs) - self.setToolTip('Click to deselect all data arrays') + self.setToolTip("Click to deselect all data arrays") self.clicked.connect(self.select_none) Project.oncpchange.connect(self.enable_from_project) @@ -344,8 +341,8 @@ class ProjectContentWidget(QWidget, DockMixin): super(ProjectContentWidget, self).__init__(*args, **kwargs) vbox = QVBoxLayout() # create buttons for unselecting and selecting all arrays - self.unselect_button = SelectNoneButton('Unselect all', parent=self) - self.select_all_button = SelectAllButton('Select all', parent=self) + self.unselect_button = SelectNoneButton("Unselect all", parent=self) + self.select_all_button = SelectAllButton("Select all", parent=self) button_hbox = QHBoxLayout() button_hbox.addWidget(self.unselect_button) button_hbox.addWidget(self.select_all_button) @@ -367,11 +364,11 @@ class DatasetTreeItem(QTreeWidgetItem): super(DatasetTreeItem, self).__init__(*args, **kwargs) self.variables = variables = QTreeWidgetItem(0) self.columns = columns - variables.setText(0, 'variables (%i)' % len(ds)) + variables.setText(0, "variables (%i)" % len(ds)) self.coords = coords = QTreeWidgetItem(0) - coords.setText(0, 'coords (%i)' % len(ds.coords)) + coords.setText(0, "coords (%i)" % len(ds.coords)) self.attrs = attrs = QTreeWidgetItem(0) - attrs.setText(0, 'Global Attributes (%i)' % len(ds.attrs)) + attrs.setText(0, "Global Attributes (%i)" % len(ds.attrs)) self.addChildren([variables, coords]) self.addChild(variables) self.addChild(attrs) @@ -393,53 +390,59 @@ class DatasetTreeItem(QTreeWidgetItem): item = QTreeWidgetItem(0) item.setText(0, str(vname)) for i, attr in enumerate(columns, 1): - if attr == 'dims': - item.setText(i, ', '.join(variable.dims)) + if attr == "dims": + item.setText(i, ", ".join(variable.dims)) else: - item.setText(i, str(variable.attrs.get(attr, getattr( - variable, attr, '')))) + item.setText( + i, + str( + variable.attrs.get( + attr, getattr(variable, attr, "") + ) + ), + ) if vname in ds.coords: coords.addChild(item) else: variables.addChild(item) - if rcParams['content.load_tooltips']: + if rcParams["content.load_tooltips"]: item.setToolTip( - 0, '<pre>' + escape_html(str(variable)) + '</pre>') + 0, "<pre>" + escape_html(str(variable)) + "</pre>" + ) # Add shape shape_item = QTreeWidgetItem(0) - shape_item.setText(0, 'shape') + shape_item.setText(0, "shape") shape_item.setText(1, str(variable.shape)) item.addChild(shape_item) - # Add dimensions dims_item = QTreeWidgetItem(0) - dims_item.setText(0, 'dims') - dims_item.setText(1, ', '.join(variable.dims)) + dims_item.setText(0, "dims") + dims_item.setText(1, ", ".join(variable.dims)) item.addChild(dims_item) # add open plots plots_item = QTreeWidgetItem(0) - plots_item.setText(0, 'Plots') + plots_item.setText(0, "Plots") self.refresh_plots_item(plots_item, vname) item.addChild(plots_item) # add variable attribute attrs_item = QTreeWidgetItem(0) - attrs_item.setText(0, 'Attributes') + attrs_item.setText(0, "Attributes") self.add_attrs(variable.attrs, attrs_item) item.addChild(attrs_item) # add variable encoding encoding_item = QTreeWidgetItem(0) - encoding_item.setText(0, 'Encoded attributes') + encoding_item.setText(0, "Encoded attributes") self.add_attrs(variable.encoding, encoding_item) item.addChild(encoding_item) def get_plots_item(self, item): for child in map(item.child, range(item.childCount())): - if child.text(0) == 'Plots': + if child.text(0) == "Plots": return child def refresh_plots_item(self, item, vname, mp=None, sp=None): @@ -454,13 +457,14 @@ class DatasetTreeItem(QTreeWidgetItem): if sp is None: sp = gcp() for i in range(len(mp)): - sub = mp[i:i+1] - array_info = sub.array_info(ds_description={'arr', 'num'}) + sub = mp[i : i + 1] + array_info = sub.array_info(ds_description={"arr", "num"}) arrs = sub._get_ds_descriptions(array_info).get(num, {}) - if arrs and any(vname in arr.psy.base_variables - for arr in arrs['arr']): + if arrs and any( + vname in arr.psy.base_variables for arr in arrs["arr"] + ): child = QTreeWidgetItem(0) - prefix = '*' if sub[0] in sp else '' + prefix = "*" if sub[0] in sp else "" text = sub[0].psy._short_info() child.setText(0, prefix + text) child.setToolTip(0, text) @@ -468,7 +472,6 @@ class DatasetTreeItem(QTreeWidgetItem): if expand and item.childCount(): item.setExpanded(True) - def add_attrs(self, attrs=None, item=None): if attrs is None: attrs = self.ds().attrs @@ -479,19 +482,20 @@ class DatasetTreeItem(QTreeWidgetItem): child = QTreeWidgetItem(0) child.setText(0, key) child.setText(1, str(val)) - child.setToolTip(1, '{}: {}'.format(key, str(val))) + child.setToolTip(1, "{}: {}".format(key, str(val))) item.addChild(child) class DatasetTree(QTreeWidget, DockMixin): - """A QTreeWidget showing informations on all datasets in the main project - """ + """A QTreeWidget showing informations on all datasets in the main project""" tooltips = { - 'Refresh': 'Refresh the selected dataset', - 'Refresh all': 'Refresh all datasets', - 'Add to project': ('Add this variable or a plot of it to the current ' - 'project')} + "Refresh": "Refresh the selected dataset", + "Refresh all": "Refresh all datasets", + "Add to project": ( + "Add this variable or a plot of it to the current " "project" + ), + } def __init__(self, *args, **kwargs): super(DatasetTree, self).__init__(*args, **kwargs) @@ -504,16 +508,19 @@ class DatasetTree(QTreeWidget, DockMixin): @staticmethod def is_variable(item): - return re.match(r'variables \(\d+\)', item.parent().text(0)) + return re.match(r"variables \(\d+\)", item.parent().text(0)) @staticmethod def is_coord(item): - return re.match(r'coords\(\d+\)', item.parent().text(0)) + return re.match(r"coords\(\d+\)", item.parent().text(0)) def load_variable_desc(self, item): parent = item.parent() - if parent is self or parent is None or not (self.is_variable(item) or - self.is_coord(item)): + if ( + parent is self + or parent is None + or not (self.is_variable(item) or self.is_coord(item)) + ): return if self.isColumnHidden(1): self.showColumn(1) @@ -526,7 +533,7 @@ class DatasetTree(QTreeWidget, DockMixin): if ds is None: return desc = escape_html(str(ds.variables[item.text(0)])) - item.setToolTip(0, '<pre>' + desc + '</pre>') + item.setToolTip(0, "<pre>" + desc + "</pre>") def create_dataset_tree(self): """Set up the columns and insert the :class:`DatasetTreeItem` @@ -534,7 +541,7 @@ class DatasetTree(QTreeWidget, DockMixin): self.set_columns() self.add_datasets_from_cp(gcp()) - def set_columns(self, columns=['Value']): + def set_columns(self, columns=["Value"]): """Set up the columns in the DatasetTree. Parameters @@ -544,7 +551,7 @@ class DatasetTree(QTreeWidget, DockMixin): self.setColumnCount(len(columns) + 1) if columns: self.setHeaderHidden(False) - self.setHeaderLabels(['Dataset'] + list(columns)) + self.setHeaderLabels(["Dataset"] + list(columns)) else: self.setHeaderHidden(True) self.attr_columns = columns @@ -560,7 +567,8 @@ class DatasetTree(QTreeWidget, DockMixin): if child.childCount() and child.isExpanded(): d[child.text(0)] = variables = [] for vchild in map( - child.child, range(child.childCount())): + child.child, range(child.childCount()) + ): if vchild.childCount() and vchild.isExpanded(): variables.append(vchild.text(0)) return ret @@ -586,20 +594,35 @@ class DatasetTree(QTreeWidget, DockMixin): expanded_items = self.expanded_items() # remove items from the tree self.clear() - for i, ds_desc in six.iteritems(project._get_ds_descriptions( - project.array_info(ds_description='all'))): - top_item = DatasetTreeItem(ds_desc['ds'], self.attr_columns, 0) - if ds_desc['fname'] is not None and not all( - s is None for s in ds_desc['fname']): - ds_desc['fname'] = ', '.join(map(osp.basename, - safe_list(ds_desc['fname']))) + for i, ds_desc in six.iteritems( + project._get_ds_descriptions( + project.array_info(ds_description="all") + ) + ): + top_item = DatasetTreeItem(ds_desc["ds"], self.attr_columns, 0) + if ds_desc["fname"] is not None and not all( + s is None for s in ds_desc["fname"] + ): + ds_desc["fname"] = ", ".join( + map(osp.basename, safe_list(ds_desc["fname"])) + ) else: - ds_desc['fname'] = None - top_item.setText(0, '%s%i: %s' % ( - '*' if any(any(arr is arr2 for arr2 in sp_arrs) - for arr in ds_desc['arr']) else '', - i, ds_desc['fname'])) - for arr in ds_desc['arr']: + ds_desc["fname"] = None + top_item.setText( + 0, + "%s%i: %s" + % ( + "*" + if any( + any(arr is arr2 for arr2 in sp_arrs) + for arr in ds_desc["arr"] + ) + else "", + i, + ds_desc["fname"], + ), + ) + for arr in ds_desc["arr"]: arr.psy.onbasechange.connect(self.add_datasets_from_cp) self.addTopLevelItem(top_item) self.expand_items(expanded_items) @@ -619,8 +642,9 @@ class DatasetTree(QTreeWidget, DockMixin): for child in map(top.child, range(top.childCount())): if child.text(0) in d: self.expandItem(child) - for vchild in map(child.child, - range(child.childCount())): + for vchild in map( + child.child, range(child.childCount()) + ): if vchild.text(0) in d[child.text(0)]: self.expandItem(vchild) @@ -629,24 +653,25 @@ class DatasetTree(QTreeWidget, DockMixin): item = self.itemAt(pos) parent, item_type, vname = self._get_toplevel_item(item) # ---- Refresh the selected item action - refresh_action = QAction('Refresh', self) - refresh_action.setToolTip(self.tooltips['Refresh']) + refresh_action = QAction("Refresh", self) + refresh_action.setToolTip(self.tooltips["Refresh"]) refresh_action.triggered.connect(lambda: self.refresh_items(parent)) # ---- Refresh all items action - refresh_all_action = QAction('Refresh all', self) - refresh_all_action.setToolTip(self.tooltips['Refresh all']) + refresh_all_action = QAction("Refresh all", self) + refresh_all_action.setToolTip(self.tooltips["Refresh all"]) refresh_all_action.triggered.connect(lambda: self.refresh_items()) # ---- add refresh actions menu.addActions([refresh_action, refresh_all_action]) # ---- add plot option - if item_type == 'variable': - add2p_action = QAction(f'Add new plot of {vname}', self) - add2p_action.setToolTip(self.tooltips['Add to project']) - add2p_action.triggered.connect(lambda: self.make_plot( - parent.ds(), item.text(0), True)) + if item_type == "variable": + add2p_action = QAction(f"Add new plot of {vname}", self) + add2p_action.setToolTip(self.tooltips["Add to project"]) + add2p_action.triggered.connect( + lambda: self.make_plot(parent.ds(), item.text(0), True) + ) menu.addSeparator() menu.addAction(add2p_action) @@ -658,12 +683,14 @@ class DatasetTree(QTreeWidget, DockMixin): if item is not None: item.add_variables() else: - for item in map(self.topLevelItem, - range(self.topLevelItemCount())): + for item in map( + self.topLevelItem, range(self.topLevelItemCount()) + ): item.add_variables() def make_plot(self, ds, name, exec_=None): from psyplot_gui.main import mainwindow + mainwindow.new_plots() mainwindow.plot_creator.switch2ds(ds) mainwindow.plot_creator.insert_array(safe_list(name)) @@ -680,10 +707,10 @@ class DatasetTree(QTreeWidget, DockMixin): while parent is not None: if self.is_variable(item): vname = item.text(0) - item_type = 'variable' + item_type = "variable" elif self.is_coord(item): vname = item.text(0) - item_type = 'coord' + item_type = "coord" item = item.parent() parent = item.parent() return item, item_type, vname @@ -708,7 +735,7 @@ class FiguresTreeItem(QTreeWidgetItem): :meth:`psyplot.data.InteractiveArray._short_info` and __str__ methods """ self.setText(0, self.arr().psy._short_info()) - if rcParams['content.load_tooltips']: + if rcParams["content.load_tooltips"]: self.setToolTip(0, str(self.arr())) def disconnect_from_array(self): @@ -727,7 +754,7 @@ class FiguresTree(QTreeWidget, DockMixin): def __init__(self, *args, **kwargs): super(FiguresTree, self).__init__(*args, **kwargs) - self.setHeaderLabel('Figure') + self.setHeaderLabel("Figure") Project.oncpchange.connect(self.add_figures_from_cp) self.add_figures_from_cp(gcp(True)) @@ -741,7 +768,8 @@ class FiguresTree(QTreeWidget, DockMixin): child.disconnect_from_array() for fig, arrays in six.iteritems(project.figs): item = QTreeWidgetItem(0) - item.setText(0, fig.canvas.get_window_title()) + item.setText(0, fig.canvas.manager.get_window_title()) item.addChildren( - [FiguresTreeItem(weakref.ref(arr), 0) for arr in arrays]) + [FiguresTreeItem(weakref.ref(arr), 0) for arr in arrays] + ) self.addTopLevelItem(item) diff --git a/psyplot_gui/dataframeeditor.py b/psyplot_gui/dataframeeditor.py index 03c36a4fd7051f73b2484600a7c19abdad27b667..f0b18b808b5b6d86c450beff4f8b55307e807328 100644 --- a/psyplot_gui/dataframeeditor.py +++ b/psyplot_gui/dataframeeditor.py @@ -1,42 +1,47 @@ """A widget to display and edit DataFrames""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only import os import os.path as osp -import six from functools import partial + import numpy as np +import pandas as pd +import six from psyplot.docstring import docstrings + +from psyplot_gui.common import ( + DockMixin, + LoadFromConsoleButton, + PyErrorMessage, + get_icon, +) from psyplot_gui.compat.qtcompat import ( - QWidget, QHBoxLayout, QVBoxLayout, QtCore, QLineEdit, - QPushButton, Qt, QToolButton, QIcon, QMenu, QLabel, QtGui, QApplication, - QCheckBox, QFileDialog, with_qt5, QTableView, QHeaderView, - QDockWidget) -from psyplot_gui.common import (DockMixin, get_icon, LoadFromConsoleButton, - PyErrorMessage) -import pandas as pd + QApplication, + QCheckBox, + QDockWidget, + QFileDialog, + QHBoxLayout, + QHeaderView, + QIcon, + QLabel, + QLineEdit, + QMenu, + QPushButton, + Qt, + QTableView, + QtCore, + QtGui, + QToolButton, + QVBoxLayout, + QWidget, + with_qt5, +) if six.PY2: try: @@ -54,7 +59,7 @@ LARGE_COLS = 60 REAL_NUMBER_TYPES = (float, int, np.int64, np.int32) COMPLEX_NUMBER_TYPES = (complex, np.complex64, np.complex128) -_bool_false = ['false', '0'] +_bool_false = ["false", "0"] def bool_false_check(value): @@ -63,22 +68,23 @@ def bool_false_check(value): will return True """ if value.lower() in _bool_false: - value = '' + value = "" return value class DataFrameModel(QtCore.QAbstractTableModel): - """ DataFrame Table Model""" + """DataFrame Table Model""" ROWS_TO_LOAD = 500 COLS_TO_LOAD = 40 - _format = '%0.6g' + _format = "%0.6g" - @docstrings.get_sections(base='DataFrameModel') + @docstrings.get_sections(base="DataFrameModel") @docstrings.dedent - def __init__(self, df, parent=None, index_editable=True, - dtypes_changeable=True): + def __init__( + self, df, parent=None, index_editable=True, dtypes_changeable=True + ): """ Parameters ---------- @@ -140,12 +146,12 @@ class DataFrameModel(QtCore.QAbstractTableModel): if orientation == Qt.Horizontal: if section == 0: - return six.text_type('Index') - elif isinstance(self.df_header[section-1], six.string_types): - header = self.df_header[section-1] + return six.text_type("Index") + elif isinstance(self.df_header[section - 1], six.string_types): + header = self.df_header[section - 1] return six.text_type(header) else: - return six.text_type(self.df_header[section-1]) + return six.text_type(self.df_header[section - 1]) else: return None @@ -169,7 +175,7 @@ class DataFrameModel(QtCore.QAbstractTableModel): if column == 0: return six.text_type(self.df_index[row]) else: - value = self.get_value(row, column-1) + value = self.get_value(row, column - 1) if isinstance(value, float): try: return self._format % value @@ -180,29 +186,37 @@ class DataFrameModel(QtCore.QAbstractTableModel): else: return six.text_type(value) - def sort(self, column, order=Qt.AscendingOrder, return_check=False, - report=True): + def sort( + self, column, order=Qt.AscendingOrder, return_check=False, report=True + ): """Overriding sort method""" try: ascending = order == Qt.AscendingOrder if column > 0: try: - self.df.sort_values(by=self.df.columns[column-1], - ascending=ascending, inplace=True, - kind='mergesort') + self.df.sort_values( + by=self.df.columns[column - 1], + ascending=ascending, + inplace=True, + kind="mergesort", + ) except AttributeError: # for pandas version < 0.17 - self.df.sort(columns=self.df.columns[column-1], - ascending=ascending, inplace=True, - kind='mergesort') + self.df.sort( + columns=self.df.columns[column - 1], + ascending=ascending, + inplace=True, + kind="mergesort", + ) self.update_df_index() else: self.df.sort_index(inplace=True, ascending=ascending) self.update_df_index() - except TypeError as e: + except TypeError: if report: self._parent.error_msg.showTraceback( - "<b>Failed to sort column!</b>") + "<b>Failed to sort column!</b>" + ) return False if return_check else None self.reset() return True if return_check else None @@ -211,8 +225,9 @@ class DataFrameModel(QtCore.QAbstractTableModel): """Set flags""" if index.column() == 0 and not self.index_editable: return Qt.ItemIsEnabled | Qt.ItemIsSelectable - return Qt.ItemFlags(QtCore.QAbstractTableModel.flags(self, index) | - Qt.ItemIsEditable) + return Qt.ItemFlags( + QtCore.QAbstractTableModel.flags(self, index) | Qt.ItemIsEditable + ) def setData(self, index, value, role=Qt.EditRole, change_type=None): """Cell content change""" @@ -229,33 +244,44 @@ class DataFrameModel(QtCore.QAbstractTableModel): value = np.asarray(change_type(value)) # to make sure it works icol = column - 1 self.df.iloc[:, icol] = self.df.iloc[:, icol].astype( - change_type) + change_type + ) except ValueError: - self.df.iloc[row, icol] = self.df.iloc[row, icol].astype(object) + self.df.iloc[row, icol] = self.df.iloc[row, icol].astype( + object + ) else: - current_value = self.get_value(row, column-1) if column else \ - self.df.index[row] + current_value = ( + self.get_value(row, column - 1) + if column + else self.df.index[row] + ) if isinstance(current_value, bool): value = bool_false_check(value) - supported_types = (bool,) + REAL_NUMBER_TYPES + \ - COMPLEX_NUMBER_TYPES - if (isinstance(current_value, supported_types) or - isinstance(current_value, six.string_types)): + supported_types = ( + (bool,) + REAL_NUMBER_TYPES + COMPLEX_NUMBER_TYPES + ) + if isinstance(current_value, supported_types) or isinstance( + current_value, six.string_types + ): if column: try: - self.df.iloc[row, column-1] = current_value.__class__( - value) - except ValueError as e: + self.df.iloc[ + row, column - 1 + ] = current_value.__class__(value) + except ValueError: self._parent.error_msg.showTraceback( - "<b>Failed to set value with %r!</b>" % value) + "<b>Failed to set value with %r!</b>" % value + ) return False elif self.index_editable: index = self.df.index.values.copy() try: index[row] = value - except ValueError as e: + except ValueError: self._parent.error_msg.showTraceback( - "<b>Failed to set value with %r!</b>" % value) + "<b>Failed to set value with %r!</b>" % value + ) return False self.df.index = pd.Index(index, name=self.df.index.name) self.update_df_index() @@ -263,8 +289,8 @@ class DataFrameModel(QtCore.QAbstractTableModel): return False else: self._parent.error_msg.showTraceback( - "<b>The type of the cell is not a supported type" - "</b>") + "<b>The type of the cell is not a supported type" "</b>" + ) return False self._parent.cell_edited.emit(row, column, current_value, value) return True @@ -292,15 +318,21 @@ class DataFrameModel(QtCore.QAbstractTableModel): if self.can_fetch_more(rows=rows): reminder = self.total_rows - self.rows_loaded items_to_fetch = min(reminder, self.ROWS_TO_LOAD) - self.beginInsertRows(QtCore.QModelIndex(), self.rows_loaded, - self.rows_loaded + items_to_fetch - 1) + self.beginInsertRows( + QtCore.QModelIndex(), + self.rows_loaded, + self.rows_loaded + items_to_fetch - 1, + ) self.rows_loaded += items_to_fetch self.endInsertRows() if self.can_fetch_more(columns=columns): reminder = self.total_cols - self.cols_loaded items_to_fetch = min(reminder, self.COLS_TO_LOAD) - self.beginInsertColumns(QtCore.QModelIndex(), self.cols_loaded, - self.cols_loaded + items_to_fetch - 1) + self.beginInsertColumns( + QtCore.QModelIndex(), + self.cols_loaded, + self.cols_loaded + items_to_fetch - 1, + ) self.cols_loaded += items_to_fetch self.endInsertColumns() @@ -315,7 +347,7 @@ class DataFrameModel(QtCore.QAbstractTableModel): return self.cols_loaded + 1 def update_df_index(self): - """"Update the DataFrame index""" + """ "Update the DataFrame index""" self.df_index = self.df.index.tolist() def reset(self): @@ -351,7 +383,7 @@ class DataFrameModel(QtCore.QAbstractTableModel): idx = df.index.values[0] else: try: - idx = df.index.values[irow-1:irow+1].mean() + idx = df.index.values[irow - 1 : irow + 1].mean() except TypeError: idx = df.index.values[min(irow, len(df) - 1)] else: @@ -373,8 +405,11 @@ class DataFrameModel(QtCore.QAbstractTableModel): df.set_index(new_idx_name, inplace=True, drop=True) df.index.name = idx_name self.update_df_index() - self.beginInsertRows(QtCore.QModelIndex(), self.rows_loaded, - self.rows_loaded + nrows - 1) + self.beginInsertRows( + QtCore.QModelIndex(), + self.rows_loaded, + self.rows_loaded + nrows - 1, + ) self.total_rows += nrows self.rows_loaded += nrows self.endInsertRows() @@ -385,6 +420,7 @@ class FrozenTableView(QTableView): """This class implements a table with its first column frozen For more information please see: http://doc.qt.io/qt-5/qtwidgets-itemviews-frozencolumn-example.html""" + def __init__(self, parent): """Constructor.""" QTableView.__init__(self, parent) @@ -410,19 +446,22 @@ class FrozenTableView(QTableView): self.setVerticalScrollMode(QTableView.ScrollPerPixel) self.verticalScrollBar().valueChanged.connect( - parent.verticalScrollBar().setValue) + parent.verticalScrollBar().setValue + ) parent.verticalScrollBar().valueChanged.connect( - self.verticalScrollBar().setValue) + self.verticalScrollBar().setValue + ) def update_geometry(self): """Update the frozen column size when an update occurs in its parent table""" - self.setGeometry(self.parent.verticalHeader().width() + - self.parent.frameWidth(), - self.parent.frameWidth(), - self.parent.columnWidth(0), - self.parent.viewport().height() + - self.parent.horizontalHeader().height()) + self.setGeometry( + self.parent.verticalHeader().width() + self.parent.frameWidth(), + self.parent.frameWidth(), + self.parent.columnWidth(0), + self.parent.viewport().height() + + self.parent.horizontalHeader().height(), + ) def contextMenuEvent(self, event): """Show the context Menu @@ -458,19 +497,24 @@ class DataFrameView(QTableView): self.setVerticalScrollMode(1) self.horizontalHeader().sectionResized.connect( - self.update_section_width) + self.update_section_width + ) self.verticalHeader().sectionResized.connect( - self.update_section_height) + self.update_section_height + ) self.sort_old = [None] self.header_class = self.horizontalHeader() self.header_class.sectionClicked.connect(self.sortByColumn) self.frozen_table_view.horizontalHeader().sectionClicked.connect( - self.sortByColumn) + self.sortByColumn + ) self.horizontalScrollBar().valueChanged.connect( - lambda val: self.load_more_data(val, columns=True)) + lambda val: self.load_more_data(val, columns=True) + ) self.verticalScrollBar().valueChanged.connect( - lambda val: self.load_more_data(val, rows=True)) + lambda val: self.load_more_data(val, rows=True) + ) def update_section_width(self, logical_index, old_size, new_size): """Update the horizontal width of the frozen column when a @@ -501,16 +545,16 @@ class DataFrameView(QTableView): """ current = QTableView.moveCursor(self, cursor_action, modifiers) - col_width = (self.columnWidth(0) + - self.columnWidth(1)) + col_width = self.columnWidth(0) + self.columnWidth(1) topleft_x = self.visualRect(current).topLeft().x() overflow = self.MoveLeft and current.column() > 1 overflow = overflow and topleft_x < col_width if cursor_action == overflow: - new_value = (self.horizontalScrollBar().value() + - topleft_x - col_width) + new_value = ( + self.horizontalScrollBar().value() + topleft_x - col_width + ) self.horizontalScrollBar().setValue(new_value) return current @@ -532,7 +576,7 @@ class DataFrameView(QTableView): self.model().fetch_more(columns=columns) def sortByColumn(self, index): - """ Implement a Column sort """ + """Implement a Column sort""" frozen_header = self.frozen_table_view.horizontalHeader() if not self.isSortingEnabled(): self.header_class.setSortIndicatorShown(False) @@ -550,11 +594,13 @@ class DataFrameView(QTableView): self.header_class.setSortIndicatorShown(False) frozen_header.setSortIndicatorShown(False) else: - self.header_class.setSortIndicator(self.sort_old[0], - self.sort_old[1]) + self.header_class.setSortIndicator( + self.sort_old[0], self.sort_old[1] + ) if index == 0: - frozen_header.setSortIndicator(self.sort_old[0], - self.sort_old[1]) + frozen_header.setSortIndicator( + self.sort_old[0], self.sort_old[1] + ) return self.sort_old = [index, self.header_class.sortIndicatorOrder()] @@ -563,7 +609,7 @@ class DataFrameView(QTableView): model = self.model() index_list = self.selectedIndexes() for i in index_list: - model.setData(i, '', change_type=func) + model.setData(i, "", change_type=func) def insert_row_above_selection(self): """Insert rows above the selection @@ -601,7 +647,7 @@ class DataFrameView(QTableView): return [], [] return list(zip(*[(i.row(), i.column()) for i in index_list])) - docstrings.delete_params('DataFrameModel.parameters', 'parent') + docstrings.delete_params("DataFrameModel.parameters", "parent") @docstrings.dedent def set_df(self, df, *args, **kwargs): @@ -625,10 +671,12 @@ class DataFrameView(QTableView): for a in self.dtype_actions.values(): a.setEnabled(model.dtypes_changeable) nrows = max(len(set(self._selected_rows_and_cols()[0])), 1) - self.insert_row_above_action.setText('Insert %i row%s above' % ( - nrows, 's' if nrows - 1 else '')) - self.insert_row_below_action.setText('Insert %i row%s below' % ( - nrows, 's' if nrows - 1 else '')) + self.insert_row_above_action.setText( + "Insert %i row%s above" % (nrows, "s" if nrows - 1 else "") + ) + self.insert_row_below_action.setText( + "Insert %i row%s below" % (nrows, "s" if nrows - 1 else "") + ) self.insert_row_above_action.setEnabled(model.index_editable) self.insert_row_below_action.setEnabled(model.index_editable) self.menu.popup(event.globalPos()) @@ -637,24 +685,33 @@ class DataFrameView(QTableView): def setup_menu(self): """Setup context menu""" menu = QMenu(self) - menu.addAction('Copy', self.copy, QtGui.QKeySequence.Copy) + menu.addAction("Copy", self.copy, QtGui.QKeySequence.Copy) menu.addSeparator() - functions = (("To bool", bool), ("To complex", complex), - ("To int", int), ("To float", float), - ("To str", str)) + functions = ( + ("To bool", bool), + ("To complex", complex), + ("To int", int), + ("To float", float), + ("To str", str), + ) self.dtype_actions = { name: menu.addAction(name, partial(self.change_type, func)) - for name, func in functions} + for name, func in functions + } menu.addSeparator() self.insert_row_above_action = menu.addAction( - 'Insert rows above', self.insert_row_above_selection) + "Insert rows above", self.insert_row_above_selection + ) self.insert_row_below_action = menu.addAction( - 'Insert rows below', self.insert_row_below_selection) + "Insert rows below", self.insert_row_below_selection + ) menu.addSeparator() self.set_index_action = menu.addAction( - 'Set as index', partial(self.set_index, False)) + "Set as index", partial(self.set_index, False) + ) self.append_index_action = menu.addAction( - 'Append to as index', partial(self.set_index, True)) + "Append to as index", partial(self.set_index, True) + ) return menu def set_index(self, append=False): @@ -672,8 +729,9 @@ class DataFrameView(QTableView): if len(cols) == 1: df.set_index(df.columns[cols[0]], inplace=True, append=append) else: - df.set_index(df.columns[cols].tolist(), inplace=True, - append=append) + df.set_index( + df.columns[cols].tolist(), inplace=True, append=append + ) self.set_df(df, *args) def copy(self): @@ -689,18 +747,21 @@ class DataFrameView(QTableView): index = True df = self.model().df if col_max == 0: # To copy indices - contents = '\n'.join(map(str, df.index.tolist()[slice(row_min, - row_max+1)])) + contents = "\n".join( + map(str, df.index.tolist()[slice(row_min, row_max + 1)]) + ) else: # To copy DataFrame if (col_min == 0 or col_min == 1) and (df.shape[1] == col_max): header = True - obj = df.iloc[slice(row_min, row_max+1), slice(col_min-1, col_max)] + obj = df.iloc[ + slice(row_min, row_max + 1), slice(col_min - 1, col_max) + ] output = io.StringIO() - obj.to_csv(output, sep='\t', index=index, header=header) + obj.to_csv(output, sep="\t", index=index, header=header) if not six.PY2: contents = output.getvalue() else: - contents = output.getvalue().decode('utf-8') + contents = output.getvalue().decode("utf-8") output.close() clipboard = QApplication.clipboard() clipboard.setText(contents) @@ -722,7 +783,7 @@ class DataFrameDock(QDockWidget): mainwindow.removeDockWidget(self) except Exception: pass - if getattr(self.widget(), '_view_action', None) is not None: + if getattr(self.widget(), "_view_action", None) is not None: mainwindow.dataframe_menu.removeAction(self.widget()._view_action) return super(DataFrameDock, self).close() @@ -757,21 +818,21 @@ class DataFrameEditor(DockMixin, QWidget): self.lbl_size = QLabel() # A Checkbox for enabling and disabling the editability of the index - self.cb_index_editable = QCheckBox('Index editable') + self.cb_index_editable = QCheckBox("Index editable") # A checkbox for enabling and disabling the change of data types - self.cb_dtypes_changeable = QCheckBox('Datatypes changeable') + self.cb_dtypes_changeable = QCheckBox("Datatypes changeable") # A checkbox for enabling and disabling sorting - self.cb_enable_sort = QCheckBox('Enable sorting') + self.cb_enable_sort = QCheckBox("Enable sorting") # A button to open a dataframe from the file self.btn_open_df = QToolButton(parent=self) - self.btn_open_df.setIcon(QIcon(get_icon('run_arrow.png'))) - self.btn_open_df.setToolTip('Open a DataFrame from your disk') + self.btn_open_df.setIcon(QIcon(get_icon("run_arrow.png"))) + self.btn_open_df.setToolTip("Open a DataFrame from your disk") self.btn_from_console = LoadFromConsoleButton(pd.DataFrame) - self.btn_from_console.setToolTip('Show a DataFrame from the console') + self.btn_from_console.setToolTip("Show a DataFrame from the console") # The table to display the DataFrame self.table = DataFrameView(pd.DataFrame(), self) @@ -781,22 +842,23 @@ class DataFrameEditor(DockMixin, QWidget): self.format_editor.setText(self.table.model()._format) # format update button - self.btn_change_format = QPushButton('Update') + self.btn_change_format = QPushButton("Update") self.btn_change_format.setEnabled(False) # table clearing button - self.btn_clear = QPushButton('Clear') + self.btn_clear = QPushButton("Clear") self.btn_clear.setToolTip( - 'Clear the table and disconnect from the DataFrame') + "Clear the table and disconnect from the DataFrame" + ) # refresh button self.btn_refresh = QToolButton() - self.btn_refresh.setIcon(QIcon(get_icon('refresh.png'))) - self.btn_refresh.setToolTip('Refresh the table') + self.btn_refresh.setIcon(QIcon(get_icon("refresh.png"))) + self.btn_refresh.setToolTip("Refresh the table") # close button - self.btn_close = QPushButton('Close') - self.btn_close.setToolTip('Close this widget permanentely') + self.btn_close = QPushButton("Close") + self.btn_close.setToolTip("Close this widget permanentely") # --------------------------------------------------------------------- # ------------------------ layout -------------------------------- @@ -826,7 +888,8 @@ class DataFrameEditor(DockMixin, QWidget): # ------------------------ Connections -------------------------------- # --------------------------------------------------------------------- self.cb_dtypes_changeable.stateChanged.connect( - self.set_dtypes_changeable) + self.set_dtypes_changeable + ) self.cb_index_editable.stateChanged.connect(self.set_index_editable) self.btn_from_console.object_loaded.connect(self._open_ds_from_console) self.rows_inserted.connect(lambda i, n: self.set_lbl_size_text()) @@ -838,11 +901,12 @@ class DataFrameEditor(DockMixin, QWidget): self.btn_refresh.clicked.connect(self.table.reset_model) self.btn_open_df.clicked.connect(self._open_dataframe) self.table.set_index_action.triggered.connect( - self.update_index_editable) + self.update_index_editable + ) self.table.append_index_action.triggered.connect( - self.update_index_editable) - self.cb_enable_sort.stateChanged.connect( - self.table.setSortingEnabled) + self.update_index_editable + ) + self.cb_enable_sort.stateChanged.connect(self.table.setSortingEnabled) def update_index_editable(self): model = self.table.model() @@ -857,9 +921,9 @@ class DataFrameEditor(DockMixin, QWidget): nrows = nrows if nrows is not None else model.rowCount() ncols = ncols if ncols is not None else model.columnCount() if not nrows and not ncols: - self.lbl_size.setText('') + self.lbl_size.setText("") else: - self.lbl_size.setText('Rows: %i, Columns: %i' % (nrows, ncols)) + self.lbl_size.setText("Rows: %i, Columns: %i" % (nrows, ncols)) def clear_table(self): """Clear the table and emit the :attr:`cleared` signal""" @@ -880,7 +944,7 @@ class DataFrameEditor(DockMixin, QWidget): show: bool If True (default), show and raise_ the editor """ - show = kwargs.pop('show', True) + show = kwargs.pop("show", True) self.table.set_df(df, *args, **kwargs) self.set_lbl_size_text(*df.shape) model = self.table.model() @@ -912,7 +976,8 @@ class DataFrameEditor(DockMixin, QWidget): self.btn_change_format.setEnabled(False) else: self.btn_change_format.setEnabled( - text.strip() != self.table.model()._format) + text.strip() != self.table.model()._format + ) def update_format(self): """Update the format of the table""" @@ -927,7 +992,8 @@ class DataFrameEditor(DockMixin, QWidget): def maybe_tabify(self): main = self.dock.parent() if self.is_shown and main.dockWidgetArea( - main.help_explorer.dock) == main.dockWidgetArea(self.dock): + main.help_explorer.dock + ) == main.dockWidgetArea(self.dock): main.tabifyDockWidget(main.help_explorer.dock, self.dock) def _open_dataframe(self): @@ -937,12 +1003,14 @@ class DataFrameEditor(DockMixin, QWidget): """Opens a file dialog and the dataset that has been inserted""" if fname is None: fname = QFileDialog.getOpenFileName( - self, 'Open dataset', os.getcwd(), - 'Comma separated files (*.csv);;' - 'Excel files (*.xls *.xlsx);;' - 'JSON files (*.json);;' - 'All files (*)' - ) + self, + "Open dataset", + os.getcwd(), + "Comma separated files (*.csv);;" + "Excel files (*.xls *.xlsx);;" + "JSON files (*.json);;" + "All files (*)", + ) if with_qt5: # the filter is passed as well fname = fname[0] if isinstance(fname, pd.DataFrame): @@ -952,18 +1020,20 @@ class DataFrameEditor(DockMixin, QWidget): else: ext = osp.splitext(fname)[1] open_funcs = { - '.xls': pd.read_excel, '.xlsx': pd.read_excel, - '.json': pd.read_json, - '.tab': partial(pd.read_csv, delimiter='\t'), - '.dat': partial(pd.read_csv, delim_whitespace=True), - } + ".xls": pd.read_excel, + ".xlsx": pd.read_excel, + ".json": pd.read_json, + ".tab": partial(pd.read_csv, delimiter="\t"), + ".dat": partial(pd.read_csv, delim_whitespace=True), + } open_func = open_funcs.get(ext, pd.read_csv) try: df = open_func(fname) except Exception: self.error_msg.showTraceback( - '<b>Could not open DataFrame %s with %s</b>' % ( - fname, open_func)) + "<b>Could not open DataFrame %s with %s</b>" + % (fname, open_func) + ) return self.set_df(df) diff --git a/psyplot_gui/dependencies.py b/psyplot_gui/dependencies.py index 6535ce73d023ad9bb83db00e7bca0d05f6a3af48..450774420bfc3347bc7f938cd738dab3c5600bae 100644 --- a/psyplot_gui/dependencies.py +++ b/psyplot_gui/dependencies.py @@ -3,35 +3,32 @@ This module defines the :class:`DependenciesWidget` that shows the versions of of psyplot, psyplot_gui, psyplot plugins and their requirements""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only -from psyplot_gui.compat.qtcompat import ( - QDialog, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QLabel, QMenu, QAction, - Qt, QApplication, QMessageBox, QPushButton, QHBoxLayout, QAbstractItemView, - QDialogButtonBox, QtCore) from psyplot.docstring import docstrings +from psyplot_gui.compat.qtcompat import ( + QAbstractItemView, + QAction, + QApplication, + QDialog, + QDialogButtonBox, + QHBoxLayout, + QLabel, + QMenu, + QMessageBox, + QPushButton, + Qt, + QtCore, + QTreeWidget, + QTreeWidgetItem, + QVBoxLayout, +) + class DependenciesTree(QTreeWidget): """A tree widget to display dependencies @@ -40,7 +37,7 @@ class DependenciesTree(QTreeWidget): :func:`psyplot.get_versions` function to display the requirements and versions.""" - @docstrings.get_sections(base='DependenciesTree') + @docstrings.get_sections(base="DependenciesTree") def __init__(self, versions, *args, **kwargs): """ Parameters @@ -55,7 +52,7 @@ class DependenciesTree(QTreeWidget): super(DependenciesTree, self).__init__(*args, **kwargs) self.resizeColumnToContents(0) self.setColumnCount(2) - self.setHeaderLabels(['Package', 'installed version']) + self.setHeaderLabels(["Package", "installed version"]) self.add_dependencies(versions) self.expandAll() self.resizeColumnToContents(0) @@ -82,15 +79,15 @@ class DependenciesTree(QTreeWidget): new_item = QTreeWidgetItem(0) new_item.setText(0, pkg) if isinstance(pkg_d, dict): - new_item.setText(1, pkg_d['version']) + new_item.setText(1, pkg_d["version"]) else: new_item.setText(1, pkg_d) if parent is None: self.addTopLevelItem(new_item) else: parent.addChild(new_item) - if 'requirements' in pkg_d: - self.add_dependencies(pkg_d['requirements'], new_item) + if "requirements" in pkg_d: + self.add_dependencies(pkg_d["requirements"], new_item) def open_menu(self, position): """Open a menu to expand and collapse all items in the tree @@ -100,10 +97,10 @@ class DependenciesTree(QTreeWidget): position: QPosition The position where to open the menu""" menu = QMenu() - expand_all_action = QAction('Expand all', self) + expand_all_action = QAction("Expand all", self) expand_all_action.triggered.connect(self.expandAll) menu.addAction(expand_all_action) - collapse_all_action = QAction('Collapse all', self) + collapse_all_action = QAction("Collapse all", self) collapse_all_action.triggered.connect(self.collapseAll) menu.addAction(collapse_all_action) menu.exec_(self.viewport().mapToGlobal(position)) @@ -138,15 +135,18 @@ class DependenciesDialog(QDialog): %(DependenciesTree.parameters)s """ super(DependenciesDialog, self).__init__(*args, **kwargs) - self.setWindowTitle('Dependencies') + self.setWindowTitle("Dependencies") self.versions = versions self.vbox = layout = QVBoxLayout() - self.label = QLabel(""" + self.label = QLabel( + """ psyplot and the plugins depend on several python libraries. The tree widget below lists the versions of the plugins and the requirements. You can select the items in the tree and copy them to - clipboard.""", parent=self) + clipboard.""", + parent=self, + ) layout.addWidget(self.label) @@ -155,9 +155,10 @@ class DependenciesDialog(QDialog): layout.addWidget(self.tree) # copy button - self.bt_copy = QPushButton('Copy selection to clipboard') + self.bt_copy = QPushButton("Copy selection to clipboard") self.bt_copy.setToolTip( - 'Copy the selected packages in the above table to the clipboard.') + "Copy the selected packages in the above table to the clipboard." + ) self.bt_copy.clicked.connect(lambda: self.copy_selected()) self.bbox = QDialogButtonBox(QDialogButtonBox.Ok) @@ -170,7 +171,7 @@ class DependenciesDialog(QDialog): layout.addLayout(hbox) #: A label for simple status update - self.info_label = QLabel('', self) + self.info_label = QLabel("", self) layout.addWidget(self.info_label) self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.clear_label) @@ -182,18 +183,20 @@ class DependenciesDialog(QDialog): d = {} items = self.tree.selectedItems() if not items: - QMessageBox.warning(self, "No packages selected!", - "Please select packages in the tree!") + QMessageBox.warning( + self, + "No packages selected!", + "Please select packages in the tree!", + ) return for item in items: d[item.text(0)] = item.text(1) if label is None: label = QApplication.clipboard() - label.setText("\n".join( - '%s: %s' % t for t in d.items())) - self.info_label.setText('Packages copied to clipboard.') + label.setText("\n".join("%s: %s" % t for t in d.items())) + self.info_label.setText("Packages copied to clipboard.") self.timer.start(3000) def clear_label(self): """Clear the info label""" - self.info_label.setText('') + self.info_label.setText("") diff --git a/psyplot_gui/fmt_widget.py b/psyplot_gui/fmt_widget.py index b919cf56fa28eb9bc7b5a7e7976afb7e34a5a1d5..bf680fc0acfd5f3dec94046bc1c9b08856eeca90 100644 --- a/psyplot_gui/fmt_widget.py +++ b/psyplot_gui/fmt_widget.py @@ -2,50 +2,47 @@ """Module defining a widget for updating the formatoption of the current project""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only -import six -import yaml -from functools import partial +import logging from collections import defaultdict +from functools import partial from itertools import chain -import logging from warnings import warn -from IPython.core.interactiveshell import ExecutionResult + +import psyplot.plotter as psyp import psyplot.project as psy +import six +import yaml +from IPython.core.interactiveshell import ExecutionResult +from psyplot.data import isstring, safe_list from psyplot.utils import _temp_bool_prop, unique_everseen + +from psyplot_gui.common import DockMixin, PyErrorMessage, get_icon from psyplot_gui.compat.qtcompat import ( - QWidget, QHBoxLayout, QComboBox, QLineEdit, QVBoxLayout, QToolButton, - QIcon, QPushButton, QCheckBox, QTextEdit, QListView, QCompleter, Qt, - QStandardItemModel, QStandardItem, with_qt5) -from psyplot_gui.plot_creator import CoordComboBox + QCheckBox, + QComboBox, + QCompleter, + QHBoxLayout, + QIcon, + QLineEdit, + QListView, + QPushButton, + QStandardItem, + QStandardItemModel, + Qt, + QTextEdit, + QToolButton, + QVBoxLayout, + QWidget, + with_qt5, +) from psyplot_gui.config.rcsetup import rcParams -from psyplot.compat.pycompat import OrderedDict, map -from psyplot_gui.common import DockMixin, get_icon, PyErrorMessage -from psyplot.data import safe_list -import psyplot.plotter as psyp -from psyplot.data import isstring +from psyplot_gui.plot_creator import CoordComboBox try: from IPython.core.interactiveshell import ExecutionInfo @@ -56,8 +53,8 @@ except ImportError: logger = logging.getLogger(__name__) -COORDSGROUP = '__coords' -ALLGROUP = '__all' +COORDSGROUP = "__coords" +ALLGROUP = "__all" class DimensionsWidget(QWidget): @@ -66,8 +63,8 @@ class DimensionsWidget(QWidget): def __init__(self, parent, dim=None): super(DimensionsWidget, self).__init__(parent) self.coord_combo = CoordComboBox(self.get_ds, dim) - self.cb_use_coord = QCheckBox('show coordinates') - self.cb_close_popups = QCheckBox('close dropdowns') + self.cb_use_coord = QCheckBox("show coordinates") + self.cb_close_popups = QCheckBox("close dropdowns") self.cb_close_popups.setChecked(True) self.toggle_close_popup() self._single_selection = False @@ -104,13 +101,15 @@ class DimensionsWidget(QWidget): inserts = list( ind.row() - 1 for ind in cb.view().selectionModel().selectedIndexes() - if ind.row() > 0) + if ind.row() > 0 + ) if not inserts: return elif not self._single_selection: try: - current = yaml.load(self.parent().get_text(), - Loader=yaml.Loader) + current = yaml.load( + self.parent().get_text(), Loader=yaml.Loader + ) except Exception: pass else: @@ -124,23 +123,26 @@ class DimensionsWidget(QWidget): def get_ds(self): import psyplot.project as psy + project = psy.gcp() datasets = project.datasets dim = self.dim dims = {ds.coords[dim].shape[0]: ds for ds in datasets.values()} if len(dims) > 1: - warn("Datasets have differing dimensions lengths for the " - "%s dimension!" % dim) + warn( + "Datasets have differing dimensions lengths for the " + "%s dimension!" % dim + ) return min(dims.items())[1] def set_single_selection(self, yes=True): self._single_selection = yes if yes: - self.coord_combo.view().setSelectionMode( - QListView.SingleSelection) + self.coord_combo.view().setSelectionMode(QListView.SingleSelection) else: self.coord_combo.view().setSelectionMode( - QListView.ExtendedSelection) + QListView.ExtendedSelection + ) class FormatoptionWidget(QWidget, DockMixin): @@ -159,7 +161,8 @@ class FormatoptionWidget(QWidget, DockMixin): """ no_fmtos_update = _temp_bool_prop( - 'no_fmtos_update', """update the fmto combo box or not""") + "no_fmtos_update", """update the fmto combo box or not""" + ) #: The combobox for the formatoption groups group_combo = None @@ -205,8 +208,8 @@ class FormatoptionWidget(QWidget, DockMixin): ``*args, **kwargs`` Any other keyword for the QWidget class """ - help_explorer = kwargs.pop('help_explorer', None) - console = kwargs.pop('console', None) + help_explorer = kwargs.pop("help_explorer", None) + console = kwargs.pop("console", None) super(FormatoptionWidget, self).__init__(*args, **kwargs) self.help_explorer = help_explorer self.console = console @@ -225,9 +228,9 @@ class FormatoptionWidget(QWidget, DockMixin): self.fmt_combo.setEditable(True) self.fmt_combo.setInsertPolicy(QComboBox.NoInsert) self.fmto_completer = completer = QCompleter( - ['time', 'lat', 'lon', 'lev']) - completer.setCompletionMode( - QCompleter.PopupCompletion) + ["time", "lat", "lon", "lev"] + ) + completer.setCompletionMode(QCompleter.PopupCompletion) completer.activated[str].connect(self.set_fmto) if with_qt5: completer.setFilterMode(Qt.MatchContains) @@ -237,19 +240,19 @@ class FormatoptionWidget(QWidget, DockMixin): self.dim_widget = DimensionsWidget(parent=self) self.dim_widget.setVisible(False) - self.multiline_button = QPushButton('Multiline', parent=self) + self.multiline_button = QPushButton("Multiline", parent=self) self.multiline_button.setCheckable(True) - self.yaml_cb = QCheckBox('Yaml syntax') + self.yaml_cb = QCheckBox("Yaml syntax") self.yaml_cb.setChecked(True) - self.keys_button = QPushButton('Keys', parent=self) - self.summaries_button = QPushButton('Summaries', parent=self) - self.docs_button = QPushButton('Docs', parent=self) + self.keys_button = QPushButton("Keys", parent=self) + self.summaries_button = QPushButton("Summaries", parent=self) + self.docs_button = QPushButton("Docs", parent=self) - self.grouped_cb = QCheckBox('grouped', parent=self) - self.all_groups_cb = QCheckBox('all groups', parent=self) - self.include_links_cb = QCheckBox('include links', parent=self) + self.grouped_cb = QCheckBox("grouped", parent=self) + self.all_groups_cb = QCheckBox("all groups", parent=self) + self.include_links_cb = QCheckBox("include links", parent=self) self.text_edit.setVisible(False) @@ -257,39 +260,48 @@ class FormatoptionWidget(QWidget, DockMixin): # -------------------------- Descriptions ----------------------------- # --------------------------------------------------------------------- - self.group_combo.setToolTip('Select the formatoption group') - self.fmt_combo.setToolTip('Select the formatoption to update') + self.group_combo.setToolTip("Select the formatoption group") + self.fmt_combo.setToolTip("Select the formatoption to update") self.line_edit.setToolTip( - 'Insert the value which what you want to update the selected ' - 'formatoption and hit right button. The code is executed in the ' - 'main console.') + "Insert the value which what you want to update the selected " + "formatoption and hit right button. The code is executed in the " + "main console." + ) self.yaml_cb.setToolTip( "Use the yaml syntax for the values inserted in the above cell. " "Otherwise the content there is evaluated as a python expression " - "in the terminal") + "in the terminal" + ) self.text_edit.setToolTip(self.line_edit.toolTip()) - self.run_button.setIcon(QIcon(get_icon('run_arrow.png'))) - self.run_button.setToolTip('Update the selected formatoption') + self.run_button.setIcon(QIcon(get_icon("run_arrow.png"))) + self.run_button.setToolTip("Update the selected formatoption") self.multiline_button.setToolTip( - 'Allow linebreaks in the text editor line above.') + "Allow linebreaks in the text editor line above." + ) self.keys_button.setToolTip( - 'Show the formatoption keys in this group (or in all ' - 'groups) in the help explorer') + "Show the formatoption keys in this group (or in all " + "groups) in the help explorer" + ) self.summaries_button.setToolTip( - 'Show the formatoption summaries in this group (or in all ' - 'groups) in the help explorer') + "Show the formatoption summaries in this group (or in all " + "groups) in the help explorer" + ) self.docs_button.setToolTip( - 'Show the formatoption documentations in this group (or in all ' - 'groups) in the help explorer') + "Show the formatoption documentations in this group (or in all " + "groups) in the help explorer" + ) self.grouped_cb.setToolTip( - 'Group the formatoptions before displaying them in the help ' - 'explorer') - self.all_groups_cb.setToolTip('Use all groups when displaying the ' - 'keys, docs or summaries') + "Group the formatoptions before displaying them in the help " + "explorer" + ) + self.all_groups_cb.setToolTip( + "Use all groups when displaying the " "keys, docs or summaries" + ) self.include_links_cb.setToolTip( - 'Include links to remote documentations when showing the ' - 'keys, docs and summaries in the help explorer (requires ' - 'intersphinx)') + "Include links to remote documentations when showing the " + "keys, docs and summaries in the help explorer (requires " + "intersphinx)" + ) # --------------------------------------------------------------------- # -------------------------- Connections ------------------------------ @@ -298,16 +310,20 @@ class FormatoptionWidget(QWidget, DockMixin): self.fmt_combo.currentIndexChanged[int].connect(self.show_fmt_info) self.fmt_combo.currentIndexChanged[int].connect(self.load_fmt_widget) self.fmt_combo.currentIndexChanged[int].connect( - self.set_current_fmt_value) + self.set_current_fmt_value + ) self.run_button.clicked.connect(self.run_code) self.line_edit.returnPressed.connect(self.run_button.click) self.multiline_button.clicked.connect(self.toggle_line_edit) self.keys_button.clicked.connect( - partial(self.show_all_fmt_info, 'keys')) + partial(self.show_all_fmt_info, "keys") + ) self.summaries_button.clicked.connect( - partial(self.show_all_fmt_info, 'summaries')) + partial(self.show_all_fmt_info, "summaries") + ) self.docs_button.clicked.connect( - partial(self.show_all_fmt_info, 'docs')) + partial(self.show_all_fmt_info, "docs") + ) # --------------------------------------------------------------------- # ------------------------------ Layouts ------------------------------ @@ -325,8 +341,14 @@ class FormatoptionWidget(QWidget, DockMixin): self.info_box.addWidget(self.multiline_button) self.info_box.addWidget(self.yaml_cb) self.info_box.addStretch(0) - for w in [self.keys_button, self.summaries_button, self.docs_button, - self.all_groups_cb, self.grouped_cb, self.include_links_cb]: + for w in [ + self.keys_button, + self.summaries_button, + self.docs_button, + self.all_groups_cb, + self.grouped_cb, + self.include_links_cb, + ]: self.info_box.addWidget(w) self.vbox = QVBoxLayout() @@ -342,10 +364,11 @@ class FormatoptionWidget(QWidget, DockMixin): # fill with content self.fill_combos_from_project(psy.gcp()) psy.Project.oncpchange.connect(self.fill_combos_from_project) - rcParams.connect('fmt.sort_by_key', self.refill_from_rc) + rcParams.connect("fmt.sort_by_key", self.refill_from_rc) def refill_from_rc(self, sort_by_key): from psyplot.project import gcp + self.fill_combos_from_project(gcp()) def fill_combos_from_project(self, project): @@ -355,9 +378,11 @@ class FormatoptionWidget(QWidget, DockMixin): ---------- project: psyplot.project.Project The project to use""" - if rcParams['fmt.sort_by_key']: + if rcParams["fmt.sort_by_key"]: + def sorter(fmto): return fmto.key + else: sorter = self.get_name @@ -373,14 +398,15 @@ class FormatoptionWidget(QWidget, DockMixin): self.line_edit.setEnabled(True) # get dimensions it_vars = chain.from_iterable( - arr.psy.iter_base_variables for arr in project.arrays) + arr.psy.iter_base_variables for arr in project.arrays + ) dims = next(it_vars).dims sdims = set(dims) for var in it_vars: sdims.intersection_update(var.dims) coords = [d for d in dims if d in sdims] coords_name = [COORDSGROUP] if coords else [] - coords_verbose = ['Dimensions'] if coords else [] + coords_verbose = ["Dimensions"] if coords else [] coords = [coords] if coords else [] if len(project.plotters): @@ -390,20 +416,26 @@ class FormatoptionWidget(QWidget, DockMixin): grouped_fmts[fmto.group].append(fmto) for val in six.itervalues(grouped_fmts): val.sort(key=sorter) - grouped_fmts = OrderedDict( - sorted(six.iteritems(grouped_fmts), - key=lambda t: psyp.groups.get(t[0], t[0]))) + grouped_fmts = dict( + sorted( + six.iteritems(grouped_fmts), + key=lambda t: psyp.groups.get(t[0], t[0]), + ) + ) fmt_groups = list(grouped_fmts.keys()) # save original names self.groups = coords_name + [ALLGROUP] + fmt_groups # save verbose group names (which are used in the combo box) self.groupnames = ( - coords_verbose + ['All formatoptions'] + list( - map(lambda s: psyp.groups.get(s, s), fmt_groups))) + coords_verbose + + ["All formatoptions"] + + list(map(lambda s: psyp.groups.get(s, s), fmt_groups)) + ) # save formatoptions fmtos = list(grouped_fmts.values()) - self.fmtos = coords + [sorted( - chain(*fmtos), key=sorter)] + fmtos + self.fmtos = ( + coords + [sorted(chain(*fmtos), key=sorter)] + fmtos + ) else: self.groups = coords_name self.groupnames = coords_verbose @@ -417,12 +449,13 @@ class FormatoptionWidget(QWidget, DockMixin): """Get the name of a :class:`psyplot.plotter.Formatoption` instance""" if isinstance(fmto, six.string_types): return fmto - return '%s (%s)' % (fmto.name, fmto.key) if fmto.name else fmto.key + return "%s (%s)" % (fmto.name, fmto.key) if fmto.name else fmto.key @property def fmto(self): return self.fmtos[self.group_combo.currentIndex()][ - self.fmt_combo.currentIndex()] + self.fmt_combo.currentIndex() + ] @fmto.setter def fmto(self, value): @@ -444,27 +477,31 @@ class FormatoptionWidget(QWidget, DockMixin): multiline :attr:`text_edit` """ # switch to multiline text edit - if (self.multiline_button.isChecked() and - not self.text_edit.isVisible()): + if ( + self.multiline_button.isChecked() + and not self.text_edit.isVisible() + ): self.line_edit.setVisible(False) self.text_edit.setVisible(True) self.text_edit.setPlainText(self.line_edit.text()) - elif (not self.multiline_button.isChecked() and - not self.line_edit.isVisible()): + elif ( + not self.multiline_button.isChecked() + and not self.line_edit.isVisible() + ): self.line_edit.setVisible(True) self.text_edit.setVisible(False) self.line_edit.setText(self.text_edit.toPlainText()) def fill_fmt_combo(self, i, current_text=None): - """Fill the :attr:`fmt_combo` combobox based on the current group name - """ + """Fill the :attr:`fmt_combo` combobox based on the current group name""" if not self.no_fmtos_update: with self.no_fmtos_update: if current_text is None: current_text = self.fmt_combo.currentText() self.fmt_combo.clear() self.fmt_combo.addItems( - list(map(self.get_name, self.fmtos[i]))) + list(map(self.get_name, self.fmtos[i])) + ) ind = self.fmt_combo.findText(current_text) self.fmt_combo.setCurrentIndex(ind if ind >= 0 else 0) # update completer model @@ -478,8 +515,11 @@ class FormatoptionWidget(QWidget, DockMixin): self.fmto = name def setup_fmt_completion_model(self): - fmtos = list(unique_everseen(map( - self.get_name, chain.from_iterable(self.fmtos)))) + fmtos = list( + unique_everseen( + map(self.get_name, chain.from_iterable(self.fmtos)) + ) + ) model = self.fmto_completer.model() model.setRowCount(len(fmtos)) for i, name in enumerate(fmtos): @@ -500,12 +540,12 @@ class FormatoptionWidget(QWidget, DockMixin): group_ind = self.group_combo.currentIndex() if not self.no_fmtos_update: from psyplot.project import gcp + if self.groups[group_ind] == COORDSGROUP: dim = self.fmtos[group_ind][i] self.fmt_widget = self.dim_widget self.dim_widget.set_dim(dim) - self.dim_widget.set_single_selection( - dim not in gcp()[0].dims) + self.dim_widget.set_single_selection(dim not in gcp()[0].dims) self.dim_widget.setVisible(True) else: fmto = self.fmtos[group_ind][i] @@ -534,6 +574,7 @@ class FormatoptionWidget(QWidget, DockMixin): if not self.no_fmtos_update: if self.groups[group_ind] == COORDSGROUP: from psyplot.project import gcp + dim = self.fmtos[group_ind][i] self.set_obj(gcp().arrays[0].psy.idims[dim]) else: @@ -541,14 +582,13 @@ class FormatoptionWidget(QWidget, DockMixin): self.set_obj(fmto.value) def show_fmt_info(self, i): - """Show the documentation of the formatoption in the help explorer - """ + """Show the documentation of the formatoption in the help explorer""" group_ind = self.group_combo.currentIndex() - if (not self.no_fmtos_update and - self.groups[group_ind] != COORDSGROUP): + if not self.no_fmtos_update and self.groups[group_ind] != COORDSGROUP: fmto = self.fmtos[self.group_combo.currentIndex()][i] fmto.plotter.show_docs( - fmto.key, include_links=self.include_links_cb.isChecked()) + fmto.key, include_links=self.include_links_cb.isChecked() + ) def run_code(self): """Run the update of the project inside the :attr:`shell`""" @@ -561,18 +601,24 @@ class FormatoptionWidget(QWidget, DockMixin): group_ind = self.group_combo.currentIndex() if self.groups[group_ind] == COORDSGROUP: key = self.fmtos[group_ind][self.fmt_combo.currentIndex()] - param = 'dims' + param = "dims" else: key = self.fmtos[group_ind][self.fmt_combo.currentIndex()].key - param = 'fmt' + param = "fmt" if self.yaml_cb.isChecked(): import psyplot.project as psy + psy.gcp().update(**{key: yaml.load(text, Loader=yaml.Loader)}) else: code = "psy.gcp().update(%s={'%s': %s})" % (param, key, text) if ExecutionInfo is not None: - info = ExecutionInfo(raw_cell=code, store_history=False, - silent=True, shell_futures=False) + info = ExecutionInfo( + raw_cell=code, + store_history=False, + silent=True, + shell_futures=False, + cell_id=None, + ) e = ExecutionResult(info) else: e = ExecutionResult() @@ -626,14 +672,14 @@ class FormatoptionWidget(QWidget, DockMixin): s = '"' + obj + current + '"' else: s = '"' + current + obj + '"' - current = '' + current = "" elif isstring(obj): # add quotation marks s = '"' + obj + '"' elif not use_yaml: s = repr(obj) else: s = yaml.dump(obj, default_flow_style=True).strip() - if s.endswith('\n...'): + if s.endswith("\n..."): s = s[:-4] if use_line_edit: self.line_edit.insert(s) @@ -663,14 +709,22 @@ class FormatoptionWidget(QWidget, DockMixin): Determines what to show""" if not self.fmtos: return - if (self.all_groups_cb.isChecked() or - self.group_combo.currentIndex() < 2): - fmtos = list(chain.from_iterable( - fmto_group for i, fmto_group in enumerate(self.fmtos) - if self.groups[i] not in [ALLGROUP, COORDSGROUP])) + if ( + self.all_groups_cb.isChecked() + or self.group_combo.currentIndex() < 2 + ): + fmtos = list( + chain.from_iterable( + fmto_group + for i, fmto_group in enumerate(self.fmtos) + if self.groups[i] not in [ALLGROUP, COORDSGROUP] + ) + ) else: fmtos = self.fmtos[self.group_combo.currentIndex()] plotter = fmtos[0].plotter - getattr(plotter, 'show_' + what)( - [fmto.key for fmto in fmtos], grouped=self.grouped_cb.isChecked(), - include_links=self.include_links_cb.isChecked()) + getattr(plotter, "show_" + what)( + [fmto.key for fmto in fmtos], + grouped=self.grouped_cb.isChecked(), + include_links=self.include_links_cb.isChecked(), + ) diff --git a/psyplot_gui/help_explorer.py b/psyplot_gui/help_explorer.py index 103b0460bf231a627afb0feb66646b3f23be2138..04697fb9b59498f66d5c1f6e061f6d759e724f06 100644 --- a/psyplot_gui/help_explorer.py +++ b/psyplot_gui/help_explorer.py @@ -2,62 +2,72 @@ """Help explorer widget supplying a simple web browser and a plain text help viewer""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only -import sys +import inspect +import logging import os.path as osp +import re +import shutil +import sys from collections import namedtuple from itertools import chain -import re +from tempfile import mkdtemp + import six -import types -import inspect -import shutil -from psyplot.docstring import indent, docstrings -from psyplot.compat.pycompat import OrderedDict +from IPython.core.oinspect import getdoc, signature +from psyplot.docstring import docstrings, indent from psyplot.utils import _temp_bool_prop -from psyplot_gui.config.rcsetup import rcParams + +from psyplot_gui.common import ( + DockMixin, + PyErrorMessage, + StreamToLogger, + get_icon, + get_module_path, + is_running_tests, +) from psyplot_gui.compat.qtcompat import ( - QWidget, QHBoxLayout, QFrame, QVBoxLayout, QWebEngineView, QToolButton, - QIcon, QtCore, QComboBox, Qt, QSortFilterProxyModel, isstring, asstring, - QCompleter, QStandardItemModel, QPlainTextEdit, QAction, QMenu, with_qt5, - QtGui) -from psyplot_gui.common import get_icon, DockMixin, PyErrorMessage -from IPython.core.oinspect import signature, getdoc -import logging -from psyplot_gui.common import get_module_path, StreamToLogger, \ - is_running_tests -from tempfile import mkdtemp + QAction, + QComboBox, + QCompleter, + QFrame, + QHBoxLayout, + QIcon, + QMenu, + QPlainTextEdit, + QSortFilterProxyModel, + QStandardItemModel, + Qt, + QtCore, + QtGui, + QToolButton, + QVBoxLayout, + QWebEngineView, + QWidget, + asstring, + isstring, + with_qt5, +) +from psyplot_gui.config.rcsetup import rcParams + try: from sphinx.application import Sphinx from sphinx.pycode import ModuleAnalyzer + try: + from psyplot.sphinxext.extended_napoleon import ( + ExtendedGoogleDocstring as GoogleDocstring, + ) from psyplot.sphinxext.extended_napoleon import ( ExtendedNumpyDocstring as NumpyDocstring, - ExtendedGoogleDocstring as GoogleDocstring) + ) except ImportError: - from sphinx.ext.napoleon import NumpyDocstring, GoogleDocstring + from sphinx.ext.napoleon import GoogleDocstring, NumpyDocstring with_sphinx = True except ImportError: with_sphinx = False @@ -77,17 +87,18 @@ try: except ImportError: def file2html(fname): - return 'file://' + fname + return "file://" + fname def html2file(url): p = urlparse(asstring(url)) # skip the first '/' on windows platform - return osp.abspath(osp.join(p.netloc, - p.path[int(sys.platform == 'win32'):])) + return osp.abspath( + osp.join(p.netloc, p.path[int(sys.platform == "win32") :]) + ) -_viewers = OrderedDict() +_viewers = dict() logger = logging.getLogger(__name__) @@ -113,7 +124,8 @@ class UrlCombo(QComboBox): self.setCompleter(self.completer) self.lineEdit().textEdited[str].connect( - self.pFilterModel.setFilterFixedString) + self.pFilterModel.setFilterFixedString + ) self.completer.activated.connect(self.add_text_on_top) self.setModel(QStandardItemModel()) @@ -158,21 +170,34 @@ class UrlBrowser(QFrame): loaded. Hence it should be handled with care""" completed = _temp_bool_prop( - 'completed', "Boolean whether the html page loading is completed.", - default=True) - - url_like_re = re.compile('^\w+://') - - doc_urls = OrderedDict([ - ('startpage', 'https://startpage.com/'), - ('psyplot', 'http://psyplot.github.io/psyplot/'), - ('pyplot', 'http://matplotlib.org/api/pyplot_api.html'), - ('seaborn', 'http://stanford.edu/~mwaskom/software/seaborn/api.html'), - ('cartopy', 'http://scitools.org.uk/cartopy/docs/latest/index.html'), - ('xarray', 'http://xarray.pydata.org/en/stable/'), - ('pandas', 'http://pandas.pydata.org/pandas-docs/stable/'), - ('numpy', 'https://docs.scipy.org/doc/numpy/reference/routines.html'), - ]) + "completed", + "Boolean whether the html page loading is completed.", + default=True, + ) + + url_like_re = re.compile(r"^\w+://") + + doc_urls = dict( + [ + ("startpage", "https://startpage.com/"), + ("psyplot", "http://psyplot.github.io/psyplot/"), + ("pyplot", "http://matplotlib.org/api/pyplot_api.html"), + ( + "seaborn", + "http://stanford.edu/~mwaskom/software/seaborn/api.html", + ), + ( + "cartopy", + "http://scitools.org.uk/cartopy/docs/latest/index.html", + ), + ("xarray", "http://xarray.pydata.org/en/stable/"), + ("pandas", "http://pandas.pydata.org/pandas-docs/stable/"), + ( + "numpy", + "https://docs.scipy.org/doc/numpy/reference/routines.html", + ), + ] + ) #: The initial url showed in the webview. If None, nothing will be #: displayed @@ -230,27 +255,27 @@ class UrlBrowser(QFrame): self.bt_url_lock = QToolButton(self) # ---------------------------- buttons settings ----------------------- - self.bt_back.setIcon(QIcon(get_icon('previous.png'))) - self.bt_back.setToolTip('Go back one page') - self.bt_ahead.setIcon(QIcon(get_icon('next.png'))) - self.bt_back.setToolTip('Go forward one page') + self.bt_back.setIcon(QIcon(get_icon("previous.png"))) + self.bt_back.setToolTip("Go back one page") + self.bt_ahead.setIcon(QIcon(get_icon("next.png"))) + self.bt_back.setToolTip("Go forward one page") - self.bt_refresh.setIcon(QIcon(get_icon('refresh.png'))) - self.bt_refresh.setToolTip('Refresh the current page') + self.bt_refresh.setIcon(QIcon(get_icon("refresh.png"))) + self.bt_refresh.setToolTip("Refresh the current page") self.bt_lock.setCheckable(True) self.bt_url_lock.setCheckable(True) - if not with_qt5 and rcParams['help_explorer.online'] is None: + if not with_qt5 and rcParams["help_explorer.online"] is None: # We now that the browser can crash with Qt4, therefore we disable # the browing in the internet self.bt_url_lock.click() - rcParams['help_explorer.online'] = False - elif rcParams['help_explorer.online'] is False: + rcParams["help_explorer.online"] = False + elif rcParams["help_explorer.online"] is False: self.bt_url_lock.click() - elif rcParams['help_explorer.online'] is None: - rcParams['help_explorer.online'] = True - rcParams.connect('help_explorer.online', self.update_url_lock_from_rc) + elif rcParams["help_explorer.online"] is None: + rcParams["help_explorer.online"] = True + rcParams.connect("help_explorer.online", self.update_url_lock_from_rc) self.bt_url_lock.clicked.connect(self.toogle_url_lock) self.bt_lock.clicked.connect(self.toogle_lock) @@ -306,18 +331,18 @@ class UrlBrowser(QFrame): def browse(self, url): """Make a web browse on the given url and show the page on the Webview - widget. """ + widget.""" if self.bt_lock.isChecked(): return if not self.url_like_re.match(url): - url = 'https://' + url - if self.bt_url_lock.isChecked() and url.startswith('http'): + url = "https://" + url + if self.bt_url_lock.isChecked() and url.startswith("http"): return if not self.completed: - logger.debug('Stopping current load...') + logger.debug("Stopping current load...") self.html.stop() self.completed = True - logger.debug('Loading %s', url) + logger.debug("Loading %s", url) # we use :meth:`PyQt5.QtWebEngineWidgets.QWebEngineView.setUrl` instead # of :meth:`PyQt5.QtWebEngineWidgets.QWebEngineView.load` because that # changes the url directly and is more useful for unittests @@ -329,7 +354,7 @@ class UrlBrowser(QFrame): url = url.toString() except AttributeError: pass - logger.debug('url changed to %s', url) + logger.debug("url changed to %s", url) try: self.tb_url.setCurrentText(url) except AttributeError: # Qt4 @@ -337,29 +362,37 @@ class UrlBrowser(QFrame): self.tb_url.add_text_on_top(url, block=True) def update_url_lock_from_rc(self, online): - if (online and self.bt_url_lock.isChecked() or - not online and not self.bt_url_lock.isChecked()): + if ( + online + and self.bt_url_lock.isChecked() + or not online + and not self.bt_url_lock.isChecked() + ): self.bt_url_lock.click() def toogle_url_lock(self): """Disable (or enable) the loading of web pages in www""" bt = self.bt_url_lock offline = bt.isChecked() - bt.setIcon(QIcon(get_icon( - 'world_red.png' if offline else 'world.png'))) + bt.setIcon( + QIcon(get_icon("world_red.png" if offline else "world.png")) + ) online_message = "Go online" if not with_qt5: - online_message += ("\nWARNING: This mode is unstable under Qt4 " - "and might result in a complete program crash!") + online_message += ( + "\nWARNING: This mode is unstable under Qt4 " + "and might result in a complete program crash!" + ) bt.setToolTip(online_message if offline else "Offline mode") - if rcParams['help_explorer.online'] is offline: - rcParams['help_explorer.online'] = not offline + if rcParams["help_explorer.online"] is offline: + rcParams["help_explorer.online"] = not offline def toogle_lock(self): """Disable (or enable) the changing of the current webpage""" bt = self.bt_lock - bt.setIcon(QIcon(get_icon( - 'lock.png' if bt.isChecked() else 'lock_open.png'))) + bt.setIcon( + QIcon(get_icon("lock.png" if bt.isChecked() else "lock_open.png")) + ) bt.setToolTip("Unlock" if bt.isChecked() else "Lock to current page") @@ -369,7 +402,7 @@ class HelpMixin(object): #: Object containing the necessary fields to describe an object given to #: the help widget. The descriptor is set up by the :meth:`describe_object` #: method. - object_descriptor = namedtuple('ObjectDescriptor', ['obj', 'name']) + object_descriptor = namedtuple("ObjectDescriptor", ["obj", "name"]) #: :class:`bool` determining whether the documentation of an object can be #: shown or not @@ -377,9 +410,9 @@ class HelpMixin(object): #: :class:`bool` determining whether this class can show restructured text can_show_rst = True - @docstrings.get_sections(base='HelpMixin.show_help') + @docstrings.get_sections(base="HelpMixin.show_help") @docstrings.dedent - def show_help(self, obj, oname='', files=None): + def show_help(self, obj, oname="", files=None): """ Show the rst documentation for the given object @@ -403,10 +436,10 @@ class HelpMixin(object): ------- str The header for the documentation""" - bars = '=' * len(descriptor.name + sig) - return bars + '\n' + descriptor.name + sig + '\n' + bars + '\n' + bars = "=" * len(descriptor.name + sig) + return bars + "\n" + descriptor.name + sig + "\n" + bars + "\n" - def describe_object(self, obj, oname=''): + def describe_object(self, obj, oname=""): """Return an instance of the :attr:`object_descriptor` class Returns @@ -436,38 +469,39 @@ class HelpMixin(object): inspect module) do not fail when the object is not saved""" obj = descriptor.obj oname = descriptor.name - sig = '' + sig = "" obj_sig = obj if callable(obj): if inspect.isclass(obj): oname = oname or obj.__name__ - obj_sig = getattr(obj, '__init__', obj) - elif six.PY2 and type(obj) is types.InstanceType: - obj_sig = getattr(obj, '__call__', obj) + obj_sig = getattr(obj, "__init__", obj) + else: + obj_sig = getattr(obj, "__call__", obj) try: sig = str(signature(obj_sig)) - sig = re.sub('^\(\s*self,\s*', '(', sig) - except: - logger.debug('Failed to get signature from %s!' % (obj, ), - exc_info=True) + sig = re.sub(r"^\(\s*self,\s*", "(", sig) + except Exception: + logger.debug( + "Failed to get signature from %s!" % (obj,), exc_info=True + ) oname = oname or type(oname).__name__ head = self.header(descriptor, sig) lines = [] ds = getdoc(obj) if ds: - lines.append('') + lines.append("") lines.append(ds) - if inspect.isclass(obj) and hasattr(obj, '__init__'): + if inspect.isclass(obj) and hasattr(obj, "__init__"): init_ds = getdoc(obj.__init__) if init_ds is not None: - lines.append('\n' + init_ds) - elif hasattr(obj, '__call__'): + lines.append("\n" + init_ds) + elif hasattr(obj, "__call__"): call_ds = getdoc(obj.__call__) if call_ds and call_ds != getdoc(object.__call__): - lines.append('\n' + call_ds) + lines.append("\n" + call_ds) doc = self.process_docstring(lines, descriptor) - return head + '\n' + doc + return head + "\n" + doc def process_docstring(self, lines, descriptor): """Make final modification on the rst lines @@ -476,11 +510,11 @@ class HelpMixin(object): ------- str The docstring""" - return '\n'.join(lines) + return "\n".join(lines) - @docstrings.get_sections(base='HelpMixin.show_rst') + @docstrings.get_sections(base="HelpMixin.show_rst") @docstrings.dedent - def show_rst(self, text, oname='', descriptor=None, files=None): + def show_rst(self, text, oname="", descriptor=None, files=None): """ Abstract method which needs to be implemented by th widget to show restructured text @@ -503,8 +537,8 @@ class HelpMixin(object): """ return False - @docstrings.get_sections(base='HelpMixin.show_intro') - def show_intro(self, text=''): + @docstrings.get_sections(base="HelpMixin.show_intro") + def show_intro(self, text=""): """ Show an intro message @@ -512,9 +546,9 @@ class HelpMixin(object): ---------- s: str A string in reStructured Text format to show""" - title = 'Welcome to psyplot!' - title += '\n' + '-' * len(title) + '\n\n' - self.show_rst(title + text, 'intro') + title = "Welcome to psyplot!" + title += "\n" + "-" * len(title) + "\n\n" + self.show_rst(title + text, "intro") class TextHelp(QFrame, HelpMixin): @@ -527,7 +561,7 @@ class TextHelp(QFrame, HelpMixin): #: The :class:`PyQt5.QtWidgets.QPlainTextEdit` instance used for #: displaying the documentation self.editor = QPlainTextEdit(parent=self) - self.editor.setFont(QtGui.QFont('Courier New')) + self.editor.setFont(QtGui.QFont("Courier New")) self.vbox.addWidget(self.editor) self.setLayout(self.vbox) @@ -552,7 +586,8 @@ class UrlHelp(UrlBrowser, HelpMixin): #: the help widget. The descriptor is set up by the :meth:`describe_object` #: method and contains an additional objtype attribute object_descriptor = namedtuple( - 'ObjectDescriptor', ['obj', 'name', 'objtype']) + "ObjectDescriptor", ["obj", "name", "objtype"] + ) can_document_object = with_sphinx can_show_rst = with_sphinx @@ -564,9 +599,9 @@ class UrlHelp(UrlBrowser, HelpMixin): sphinx_thread = None def __init__(self, *args, **kwargs): - self._temp_dir = 'sphinx_dir' not in kwargs - self.sphinx_dir = kwargs.pop('sphinx_dir', mkdtemp(prefix='psyplot_')) - self.build_dir = osp.join(self.sphinx_dir, '_build', 'html') + self._temp_dir = "sphinx_dir" not in kwargs + self.sphinx_dir = kwargs.pop("sphinx_dir", mkdtemp(prefix="psyplot_")) + self.build_dir = osp.join(self.sphinx_dir, "_build", "html") super(UrlHelp, self).__init__(*args, **kwargs) self.error_msg = PyErrorMessage(self) @@ -574,39 +609,46 @@ class UrlHelp(UrlBrowser, HelpMixin): self.sphinx_thread = SphinxThread(self.sphinx_dir) self.sphinx_thread.html_ready[str].connect(self.browse) self.sphinx_thread.html_error[str].connect( - self.error_msg.showTraceback) + self.error_msg.showTraceback + ) self.sphinx_thread.html_error[str].connect(logger.debug) - rcParams.connect('help_explorer.render_docs_parallel', - self.reset_sphinx) - rcParams.connect('help_explorer.use_intersphinx', - self.reset_sphinx) - rcParams.connect('help_explorer.online', - self.reset_sphinx) + rcParams.connect( + "help_explorer.render_docs_parallel", self.reset_sphinx + ) + rcParams.connect( + "help_explorer.use_intersphinx", self.reset_sphinx + ) + rcParams.connect("help_explorer.online", self.reset_sphinx) self.bt_connect_console = QToolButton(self) self.bt_connect_console.setCheckable(True) - if rcParams['console.connect_to_help']: - self.bt_connect_console.setIcon(QIcon(get_icon( - 'ipython_console.png'))) + if rcParams["console.connect_to_help"]: + self.bt_connect_console.setIcon( + QIcon(get_icon("ipython_console.png")) + ) self.bt_connect_console.click() else: - self.bt_connect_console.setIcon(QIcon(get_icon( - 'ipython_console_t.png'))) + self.bt_connect_console.setIcon( + QIcon(get_icon("ipython_console_t.png")) + ) self.bt_connect_console.clicked.connect(self.toogle_connect_console) - rcParams.connect('console.connect_to_help', - self.update_connect_console) + rcParams.connect( + "console.connect_to_help", self.update_connect_console + ) self.toogle_connect_console() # menu button with different urls self.bt_url_menus = QToolButton(self) - self.bt_url_menus.setIcon(QIcon(get_icon('docu_button.png'))) - self.bt_url_menus.setToolTip('Browse documentations') + self.bt_url_menus.setIcon(QIcon(get_icon("docu_button.png"))) + self.bt_url_menus.setToolTip("Browse documentations") self.bt_url_menus.setPopupMode(QToolButton.InstantPopup) docu_menu = QMenu(self) for name, url in six.iteritems(self.doc_urls): + def to_url(b, url=url): self.browse(url) + action = QAction(name, self) action.triggered.connect(to_url) docu_menu.addAction(action) @@ -618,28 +660,41 @@ class UrlHelp(UrlBrowser, HelpMixin): self.toogle_url_lock() def update_connect_console(self, connect): - if (connect and not self.bt_connect_console.isChecked() or - not connect and self.bt_connect_console.isChecked()): + if ( + connect + and not self.bt_connect_console.isChecked() + or not connect + and self.bt_connect_console.isChecked() + ): self.bt_connect_console.click() def toogle_connect_console(self): """Disable (or enable) the loading of web pages in www""" bt = self.bt_connect_console connect = bt.isChecked() - bt.setIcon(QIcon(get_icon( - 'ipython_console.png' if connect else 'ipython_console_t.png'))) - bt.setToolTip("%sonnect the console to the help explorer" % ( - "Don't c" if connect else "C")) - if rcParams['console.connect_to_help'] is not connect: - rcParams['console.connect_to_help'] = connect + bt.setIcon( + QIcon( + get_icon( + "ipython_console.png" + if connect + else "ipython_console_t.png" + ) + ) + ) + bt.setToolTip( + "%sonnect the console to the help explorer" + % ("Don't c" if connect else "C") + ) + if rcParams["console.connect_to_help"] is not connect: + rcParams["console.connect_to_help"] = connect def reset_sphinx(self, value): """Method that is called if the configuration changes""" - if with_sphinx and hasattr(self.sphinx_thread, 'app'): + if with_sphinx and hasattr(self.sphinx_thread, "app"): del self.sphinx_thread.app @docstrings.dedent - def show_help(self, obj, oname='', files=None): + def show_help(self, obj, oname="", files=None): """ Render the rst docu for the given object with sphinx and show it @@ -652,7 +707,7 @@ class UrlHelp(UrlBrowser, HelpMixin): return super(UrlHelp, self).show_help(obj, oname=oname, files=files) @docstrings.dedent - def show_intro(self, text=''): + def show_intro(self, text=""): """ Show the intro text in the explorer @@ -660,13 +715,14 @@ class UrlHelp(UrlBrowser, HelpMixin): ---------- %(HelpMixin.show_intro.parameters)s""" if self.sphinx_thread is not None: - with open(self.sphinx_thread.index_file, 'a') as f: - f.write('\n' + text.strip() + '\n\n' + - 'Table of Contents\n' - '=================\n\n.. toctree::\n') + with open(self.sphinx_thread.index_file, "a") as f: + f.write( + "\n" + text.strip() + "\n\n" + "Table of Contents\n" + "=================\n\n.. toctree::\n" + ) self.sphinx_thread.render(None, None) - def show_rst(self, text, oname='', descriptor=None, files=None): + def show_rst(self, text, oname="", descriptor=None, files=None): """Render restructured text with sphinx and show it Parameters @@ -681,7 +737,7 @@ class UrlHelp(UrlBrowser, HelpMixin): self.sphinx_thread.render(text, oname) return True - def describe_object(self, obj, oname=''): + def describe_object(self, obj, oname=""): """Describe an object using additionaly the object type from the :meth:`get_objtype` method @@ -694,7 +750,7 @@ class UrlHelp(UrlBrowser, HelpMixin): def browse(self, url): """Reimplemented to add file paths to the url string""" url = asstring(url) - html_file = osp.join(self.sphinx_dir, '_build', 'html', url + '.html') + html_file = osp.join(self.sphinx_dir, "_build", "html", url + ".html") if osp.exists(html_file): url = file2html(html_file) super(UrlHelp, self).browse(url) @@ -716,17 +772,21 @@ class UrlHelp(UrlBrowser, HelpMixin): url = asstring(url.toString()) except AttributeError: pass - if url.startswith('file://'): + if url.startswith("file://"): fname = html2file(url) - if osp.samefile(self.build_dir, osp.commonprefix([ - fname, self.build_dir])): + if osp.samefile( + self.build_dir, osp.commonprefix([fname, self.build_dir]) + ): url = osp.splitext(osp.basename(fname))[0] super(UrlHelp, self).url_changed(url) def header(self, descriptor, sig): - return '%(name)s\n%(bars)s\n\n.. py:%(type)s:: %(name)s%(sig)s\n' % { - 'name': descriptor.name, 'bars': '-' * len(descriptor.name), - 'type': descriptor.objtype, 'sig': sig} + return "%(name)s\n%(bars)s\n\n.. py:%(type)s:: %(name)s%(sig)s\n" % { + "name": descriptor.name, + "bars": "-" * len(descriptor.name), + "type": descriptor.objtype, + "sig": sig, + } def get_objtype(self, obj): """Get the object type of the given object and determine wheter the @@ -741,14 +801,14 @@ class UrlHelp(UrlBrowser, HelpMixin): str One out of {'class', 'module', 'function', 'method', 'data'}""" if inspect.isclass(obj): - return 'class' + return "class" if inspect.ismodule(obj): - return 'module' + return "module" if inspect.isfunction(obj) or isinstance(obj, type(all)): - return 'function' + return "function" if inspect.ismethod(obj) or isinstance(obj, type(str.upper)): - return 'method' - return 'data' + return "method" + return "data" def is_importable(self, modname): """Determine whether members of the given module can be documented with @@ -777,70 +837,93 @@ class UrlHelp(UrlBrowser, HelpMixin): module = obj else: module = inspect.getmodule(obj) - if module is not None and (re.match('__.*__', module.__name__) or - not self.is_importable(module.__name__)): + if module is not None and ( + re.match("__.*__", module.__name__) + or not self.is_importable(module.__name__) + ): module = None isclass = inspect.isclass(obj) # If the module is available, we try to use autodoc if module is not None: - doc = '.. currentmodule:: ' + module.__name__ + '\n\n' + doc = ".. currentmodule:: " + module.__name__ + "\n\n" # a module --> use automodule if inspect.ismodule(obj): - doc += self.header(descriptor, '') - doc += '.. automodule:: ' + obj.__name__ + doc += self.header(descriptor, "") + doc += ".. automodule:: " + obj.__name__ # an importable class --> use autoclass elif isclass and getattr(module, obj.__name__, None) is not None: - doc += self.header(descriptor, '') - doc += '.. autoclass:: ' + obj.__name__ + doc += self.header(descriptor, "") + doc += ".. autoclass:: " + obj.__name__ # an instance and the class can be imported # --> use super get_doc and autoclass for the tyoe - elif descriptor.objtype == 'data' and getattr( - module, type(obj).__name__, None) is not None: - doc += '\n\n'.join([ - super(UrlHelp, self).get_doc(descriptor), - "Class docstring\n===============", - '.. autoclass:: ' + type(obj).__name__]) + elif ( + descriptor.objtype == "data" + and getattr(module, type(obj).__name__, None) is not None + ): + doc += "\n\n".join( + [ + super(UrlHelp, self).get_doc(descriptor), + "Class docstring\n===============", + ".. autoclass:: " + type(obj).__name__, + ] + ) # an instance --> use super get_doc for instance and the type - elif descriptor.objtype == 'data': - cls_doc = super(UrlHelp, self).get_doc(self.describe_object( - type(obj), type(obj).__name__)) - doc += '\n\n'.join([ - super(UrlHelp, self).get_doc(descriptor), - "Class docstring\n===============", - cls_doc]) + elif descriptor.objtype == "data": + cls_doc = super(UrlHelp, self).get_doc( + self.describe_object(type(obj), type(obj).__name__) + ) + doc += "\n\n".join( + [ + super(UrlHelp, self).get_doc(descriptor), + "Class docstring\n===============", + cls_doc, + ] + ) # a function or method --> use super get_doc else: doc += super(UrlHelp, self).get_doc(descriptor) # otherwise the object has been defined in this session else: # an instance --> use super get_doc for instance and the type - if descriptor.objtype == 'data': - cls_doc = super(UrlHelp, self).get_doc(self.describe_object( - type(obj), type(obj).__name__)) - doc = '\n\n'.join([ - super(UrlHelp, self).get_doc(descriptor), - "Class docstring\n===============", - cls_doc]) + if descriptor.objtype == "data": + cls_doc = super(UrlHelp, self).get_doc( + self.describe_object(type(obj), type(obj).__name__) + ) + doc = "\n\n".join( + [ + super(UrlHelp, self).get_doc(descriptor), + "Class docstring\n===============", + cls_doc, + ] + ) # a function or method --> use super get_doc else: doc = super(UrlHelp, self).get_doc(descriptor) - return doc.rstrip() + '\n' + return doc.rstrip() + "\n" def process_docstring(self, lines, descriptor): """Process the lines with the napoleon sphinx extension""" - lines = list(chain(*(l.splitlines() for l in lines))) + lines = list(chain(*(line.splitlines() for line in lines))) lines = NumpyDocstring( - lines, what=descriptor.objtype, name=descriptor.name, - obj=descriptor.obj).lines() + lines, + what=descriptor.objtype, + name=descriptor.name, + obj=descriptor.obj, + ).lines() lines = GoogleDocstring( - lines, what=descriptor.objtype, name=descriptor.name, - obj=descriptor.obj).lines() - return indent(super(UrlHelp, self).process_docstring( - lines, descriptor)) + lines, + what=descriptor.objtype, + name=descriptor.name, + obj=descriptor.obj, + ).lines() + return indent( + super(UrlHelp, self).process_docstring(lines, descriptor) + ) def close(self, *args, **kwargs): - if kwargs.pop('force', False) or ( - not is_running_tests() and self.sphinx_thread is not None): + if kwargs.pop("force", False) or ( + not is_running_tests() and self.sphinx_thread is not None + ): try: del self.sphinx_thread.app except AttributeError: @@ -865,17 +948,19 @@ class SphinxThread(QtCore.QThread): html_ready = QtCore.pyqtSignal(str) html_error = QtCore.pyqtSignal(str) - def __init__(self, outdir, html_text_no_doc=''): + def __init__(self, outdir, html_text_no_doc=""): super(SphinxThread, self).__init__() self.doc = None self.name = None self.html_text_no_doc = html_text_no_doc self.outdir = outdir - self.index_file = osp.join(self.outdir, 'psyplot.rst') - self.confdir = osp.join(get_module_path(__name__), 'sphinx_supp') - shutil.copyfile(osp.join(self.confdir, 'psyplot.rst'), - osp.join(self.outdir, 'psyplot.rst')) - self.build_dir = osp.join(self.outdir, '_build', 'html') + self.index_file = osp.join(self.outdir, "psyplot.rst") + self.confdir = osp.join(get_module_path(__name__), "sphinx_supp") + shutil.copyfile( + osp.join(self.confdir, "psyplot.rst"), + osp.join(self.outdir, "psyplot.rst"), + ) + self.build_dir = osp.join(self.outdir, "_build", "html") def render(self, doc, name): """Render the given rst string and save the file as ``name + '.rst'`` @@ -890,7 +975,7 @@ class SphinxThread(QtCore.QThread): self.doc = doc self.name = name # start rendering in separate process - if rcParams['help_explorer.render_docs_parallel']: + if rcParams["help_explorer.render_docs_parallel"]: self.start() else: self.run() @@ -901,40 +986,45 @@ class SphinxThread(QtCore.QThread): potentially with intersphinx When finished, the html_ready signal is emitted""" - if not hasattr(self, 'app'): + if not hasattr(self, "app"): from IPython.core.history import HistoryAccessor + # to avoid history access conflicts between different threads, # we disable the ipython history HistoryAccessor.enabled.default_value = False - self.app = Sphinx(self.outdir, - self.confdir, - self.build_dir, - osp.join(self.outdir, '_build', 'doctrees'), - 'html', - status=StreamToLogger(logger, logging.DEBUG), - warning=StreamToLogger(logger, logging.DEBUG)) + self.app = Sphinx( + self.outdir, + self.confdir, + self.build_dir, + osp.join(self.outdir, "_build", "doctrees"), + "html", + status=StreamToLogger(logger, logging.DEBUG), + warning=StreamToLogger(logger, logging.DEBUG), + ) if self.name is not None: - docfile = osp.abspath(osp.join(self.outdir, self.name + '.rst')) + docfile = osp.abspath(osp.join(self.outdir, self.name + ".rst")) if docfile == self.index_file: - self.name += '1' + self.name += "1" docfile = osp.abspath( - osp.join(self.outdir, self.name + '.rst')) - html_file = osp.abspath(osp.join( - self.outdir, '_build', 'html', self.name + '.html')) + osp.join(self.outdir, self.name + ".rst") + ) + html_file = osp.abspath( + osp.join(self.outdir, "_build", "html", self.name + ".html") + ) if not osp.exists(docfile): - with open(self.index_file, 'a') as f: - f.write('\n ' + self.name) - with open(docfile, 'w') as f: + with open(self.index_file, "a") as f: + f.write("\n " + self.name) + with open(docfile, "w") as f: f.write(self.doc) else: - html_file = osp.abspath(osp.join( - self.outdir, '_build', 'html', 'psyplot.html')) + html_file = osp.abspath( + osp.join(self.outdir, "_build", "html", "psyplot.html") + ) try: self.app.build(None, []) except Exception: - msg = 'Error while building sphinx document %s' % ( - self.name) - self.html_error.emit('<b>' + msg + '</b>') + msg = "Error while building sphinx document %s" % (self.name) + self.html_error.emit("<b>" + msg + "</b>") logger.debug(msg) else: self.html_ready.emit(file2html(html_file)) @@ -955,10 +1045,10 @@ class HelpExplorer(QWidget, DockMixin): #: The viewer classes used by the help explorer. :class:`HelpExplorer` #: instances replace this attribute with the corresponding HelpMixin #: instance - viewers = OrderedDict([('HTML help', UrlHelp), ('Plain text', TextHelp)]) + viewers = dict([("HTML help", UrlHelp), ("Plain text", TextHelp)]) - if not rcParams['help_explorer.use_webengineview']: - del viewers['HTML help'] + if not rcParams["help_explorer.use_webengineview"]: + del viewers["HTML help"] def __init__(self, *args, **kwargs): super(HelpExplorer, self).__init__(*args, **kwargs) @@ -970,9 +1060,12 @@ class HelpExplorer(QWidget, DockMixin): for w in self.viewers.values(): w.setParent(self) else: - self.viewers = OrderedDict( - [(key, cls(parent=self)) for key, cls in six.iteritems( - self.viewers)]) + self.viewers = dict( + [ + (key, cls(parent=self)) + for key, cls in six.iteritems(self.viewers) + ] + ) # save the UrlHelp because QWebEngineView creates child processes # that are not properly closed by PyQt and as such use too much # memory @@ -997,7 +1090,7 @@ class HelpExplorer(QWidget, DockMixin): A string must be one of the :attr:`viewers` attribute. An object can be one of the values in the :attr:`viewers` attribute""" if isstring(name) and asstring(name) not in self.viewers: - raise ValueError("Don't have a viewer named %s" % (name, )) + raise ValueError("Don't have a viewer named %s" % (name,)) elif not isstring(name): viewer = name else: @@ -1006,12 +1099,11 @@ class HelpExplorer(QWidget, DockMixin): self.viewer.hide() self.viewer = viewer self.viewer.show() - if (isstring(name) and - not self.combo.currentText() == name): + if isstring(name) and not self.combo.currentText() == name: self.combo.setCurrentIndex(list(self.viewers).index(name)) @docstrings.dedent - def show_help(self, obj, oname='', files=None): + def show_help(self, obj, oname="", files=None): """ Show the documentaion of the given object @@ -1028,12 +1120,17 @@ class HelpExplorer(QWidget, DockMixin): try: ret = self.viewer.show_help(obj, oname=oname, files=files) except Exception: - logger.debug("Could not document %s with %s viewer!", - oname, self.combo.currentText(), exc_info=True) + logger.debug( + "Could not document %s with %s viewer!", + oname, + self.combo.currentText(), + exc_info=True, + ) else: curr_i = self.combo.currentIndex() for i, (viewername, viewer) in enumerate( - six.iteritems(self.viewers)): + six.iteritems(self.viewers) + ): if i != curr_i and viewer.can_document_object: self.set_viewer(viewername) self.combo.blockSignals(True) @@ -1042,14 +1139,18 @@ class HelpExplorer(QWidget, DockMixin): try: ret = viewer.show_help(obj, oname=oname, files=files) except Exception: - logger.debug("Could not document %s with %s viewer!", - oname, viewername, exc_info=True) + logger.debug( + "Could not document %s with %s viewer!", + oname, + viewername, + exc_info=True, + ) if ret: self.parent().raise_() return ret @docstrings.dedent - def show_rst(self, text, oname='', files=None): + def show_rst(self, text, oname="", files=None): """ Show restructured text @@ -1074,7 +1175,7 @@ class HelpExplorer(QWidget, DockMixin): return ret @docstrings.dedent - def show_intro(self, text=''): + def show_intro(self, text=""): """ Show an intro text @@ -1095,7 +1196,7 @@ class HelpExplorer(QWidget, DockMixin): def close(self, *args, **kwargs): try: - self.viewers['HTML help'].close(*args, **kwargs) + self.viewers["HTML help"].close(*args, **kwargs) except (KeyError, AttributeError): pass return super(HelpExplorer, self).close(*args, **kwargs) diff --git a/psyplot_gui/icons/console-go.png b/psyplot_gui/icons/console-go.png index 0b44bd1403b923c0260081cecb884cb270d612d1..b63bf8ee72012570f63a78fbeefb6275563fb89f 100644 Binary files a/psyplot_gui/icons/console-go.png and b/psyplot_gui/icons/console-go.png differ diff --git a/psyplot_gui/icons/console-go.png.license b/psyplot_gui/icons/console-go.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/console-go.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/console-go.svg b/psyplot_gui/icons/console-go.svg new file mode 100644 index 0000000000000000000000000000000000000000..50f6e60f53532bfe43095b4e0faaec8c30c7d737 --- /dev/null +++ b/psyplot_gui/icons/console-go.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#63E6BE" d="M9.4 86.6C-3.1 74.1-3.1 53.9 9.4 41.4s32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L178.7 256 9.4 86.6zM256 416H544c17.7 0 32 14.3 32 32s-14.3 32-32 32H256c-17.7 0-32-14.3-32-32s14.3-32 32-32z"/></svg> diff --git a/psyplot_gui/icons/console-go.svg.license b/psyplot_gui/icons/console-go.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/console-go.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/copy.png b/psyplot_gui/icons/copy.png index 6a7614a67e5e36a35fc68a7e00ca0b6e44b8d1c5..78ba5fd4b126b330e4c5348904863baf568250ec 100644 Binary files a/psyplot_gui/icons/copy.png and b/psyplot_gui/icons/copy.png differ diff --git a/psyplot_gui/icons/copy.png.license b/psyplot_gui/icons/copy.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/copy.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/copy.svg b/psyplot_gui/icons/copy.svg new file mode 100644 index 0000000000000000000000000000000000000000..2d21436af2cf272564bffc37f2e11c548707b710 --- /dev/null +++ b/psyplot_gui/icons/copy.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M208 0H332.1c12.7 0 24.9 5.1 33.9 14.1l67.9 67.9c9 9 14.1 21.2 14.1 33.9V336c0 26.5-21.5 48-48 48H208c-26.5 0-48-21.5-48-48V48c0-26.5 21.5-48 48-48zM48 128h80v64H64V448H256V416h64v48c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V176c0-26.5 21.5-48 48-48z"/></svg> diff --git a/psyplot_gui/icons/copy.svg.license b/psyplot_gui/icons/copy.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/copy.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/docu_button.png b/psyplot_gui/icons/docu_button.png index 0b64ea9147f08a9448a625bac029028f3c41ca01..1986e4ac7072aa987d42b9816ba7c8d38bbd7bc4 100644 Binary files a/psyplot_gui/icons/docu_button.png and b/psyplot_gui/icons/docu_button.png differ diff --git a/psyplot_gui/icons/docu_button.png.license b/psyplot_gui/icons/docu_button.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/docu_button.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/docu_button.svg b/psyplot_gui/icons/docu_button.svg new file mode 100644 index 0000000000000000000000000000000000000000..5867e10b1ce3ad20eb8bb5cacfc76f2135f0df42 --- /dev/null +++ b/psyplot_gui/icons/docu_button.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M96 0C43 0 0 43 0 96V416c0 53 43 96 96 96H384h32c17.7 0 32-14.3 32-32s-14.3-32-32-32V384c17.7 0 32-14.3 32-32V32c0-17.7-14.3-32-32-32H384 96zm0 384H352v64H96c-17.7 0-32-14.3-32-32s14.3-32 32-32zm32-240c0-8.8 7.2-16 16-16H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16zm16 48H336c8.8 0 16 7.2 16 16s-7.2 16-16 16H144c-8.8 0-16-7.2-16-16s7.2-16 16-16z"/></svg> diff --git a/psyplot_gui/icons/docu_button.svg.license b/psyplot_gui/icons/docu_button.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/docu_button.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/info.png b/psyplot_gui/icons/info.png index 8671d32bf2d3ef82d4404df4924919ea076919bb..3ddc9462d02e157abb2c861a868dca0991ed0200 100644 Binary files a/psyplot_gui/icons/info.png and b/psyplot_gui/icons/info.png differ diff --git a/psyplot_gui/icons/info.png.license b/psyplot_gui/icons/info.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/info.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/info.svg b/psyplot_gui/icons/info.svg new file mode 100644 index 0000000000000000000000000000000000000000..94f36eea79e1e81bed58f340306796102aed1725 --- /dev/null +++ b/psyplot_gui/icons/info.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#74C0FC" d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg> diff --git a/psyplot_gui/icons/info.svg.license b/psyplot_gui/icons/info.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/info.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/invalid.png b/psyplot_gui/icons/invalid.png index 26e4f959efde0b46924f9dce1fca4bc2f72b1c19..aa461fc58116a7857800c4f08ec7d3c9633fdc17 100644 Binary files a/psyplot_gui/icons/invalid.png and b/psyplot_gui/icons/invalid.png differ diff --git a/psyplot_gui/icons/invalid.png.license b/psyplot_gui/icons/invalid.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/invalid.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/invalid.svg b/psyplot_gui/icons/invalid.svg new file mode 100644 index 0000000000000000000000000000000000000000..9ce28ec92f4d6e0938f21841eb905482ab8551f2 --- /dev/null +++ b/psyplot_gui/icons/invalid.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#a51d2d" d="M342.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L192 210.7 86.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L146.7 256 41.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L192 301.3 297.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L237.3 256 342.6 150.6z"/></svg> diff --git a/psyplot_gui/icons/invalid.svg.license b/psyplot_gui/icons/invalid.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/invalid.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/ipython_console.png.license b/psyplot_gui/icons/ipython_console.png.license new file mode 100644 index 0000000000000000000000000000000000000000..b21fae9960bf621e598d171baf4de286e59ac4e1 --- /dev/null +++ b/psyplot_gui/icons/ipython_console.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/ipython_console_t.png.license b/psyplot_gui/icons/ipython_console_t.png.license new file mode 100644 index 0000000000000000000000000000000000000000..b21fae9960bf621e598d171baf4de286e59ac4e1 --- /dev/null +++ b/psyplot_gui/icons/ipython_console_t.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/lock.png b/psyplot_gui/icons/lock.png index f6ef32bf1ac29e9330db8144351ac36d58fdaf49..73f3439424c8897efecd04c4ea61b7d9d1b40937 100644 Binary files a/psyplot_gui/icons/lock.png and b/psyplot_gui/icons/lock.png differ diff --git a/psyplot_gui/icons/lock.png.license b/psyplot_gui/icons/lock.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/lock.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/lock.svg b/psyplot_gui/icons/lock.svg new file mode 100644 index 0000000000000000000000000000000000000000..4cc77332efa71f620acdac0ea5d64e47eb64bb55 --- /dev/null +++ b/psyplot_gui/icons/lock.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#a51d2d" d="M144 144v48H304V144c0-44.2-35.8-80-80-80s-80 35.8-80 80zM80 192V144C80 64.5 144.5 0 224 0s144 64.5 144 144v48h16c35.3 0 64 28.7 64 64V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V256c0-35.3 28.7-64 64-64H80z"/></svg> diff --git a/psyplot_gui/icons/lock.svg.license b/psyplot_gui/icons/lock.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/lock.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/lock_open.png b/psyplot_gui/icons/lock_open.png index d0a0254c57275547bf4fa49694b34f30f67970d2..4de317a7ba920374b3db470e3437e061a1150c3b 100644 Binary files a/psyplot_gui/icons/lock_open.png and b/psyplot_gui/icons/lock_open.png differ diff --git a/psyplot_gui/icons/lock_open.png.license b/psyplot_gui/icons/lock_open.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/lock_open.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/lock_open.svg b/psyplot_gui/icons/lock_open.svg new file mode 100644 index 0000000000000000000000000000000000000000..b569c33d4d59ef157f6bb4817f54131a726746c8 --- /dev/null +++ b/psyplot_gui/icons/lock_open.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#63E6BE" d="M352 144c0-44.2 35.8-80 80-80s80 35.8 80 80v48c0 17.7 14.3 32 32 32s32-14.3 32-32V144C576 64.5 511.5 0 432 0S288 64.5 288 144v48H64c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V256c0-35.3-28.7-64-64-64H352V144z"/></svg> diff --git a/psyplot_gui/icons/lock_open.svg.license b/psyplot_gui/icons/lock_open.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/lock_open.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/logo.png.license b/psyplot_gui/icons/logo.png.license new file mode 100644 index 0000000000000000000000000000000000000000..b21fae9960bf621e598d171baf4de286e59ac4e1 --- /dev/null +++ b/psyplot_gui/icons/logo.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/logo.svg b/psyplot_gui/icons/logo.svg index 491c49e89da06cbbbb8d413543731c3f1f3360d0..fc3135943c72d43182b9803fe2af64475915af1e 100644 --- a/psyplot_gui/icons/logo.svg +++ b/psyplot_gui/icons/logo.svg @@ -10,5699 +10,5699 @@ </defs> <g id="figure_1"> <g id="patch_1"> - <path d="M 0 576 -L 576 576 -L 576 0 -L 0 0 + <path d="M 0 576 +L 576 576 +L 576 0 +L 0 0 z " style="fill:none;"/> </g> <g id="axes_1"> <g id="patch_2"> - <path d="M 576 288 -L 574.422306 318.104197 -L 569.706509 347.878567 -L 561.904277 376.996894 -L 551.101092 405.140153 -L 537.415316 432 -L 520.996894 457.282153 -L 502.02571 480.709615 -L 480.709615 502.02571 -L 457.282153 520.996894 -L 432 537.415316 -L 405.140153 551.101092 -L 376.996894 561.904277 -L 347.878567 569.706509 -L 318.104197 574.422306 -L 288 576 -L 257.895803 574.422306 -L 228.121433 569.706509 -L 199.003106 561.904277 -L 170.859847 551.101092 -L 144 537.415316 -L 118.717847 520.996894 -L 95.290385 502.02571 -L 73.97429 480.709615 -L 55.003106 457.282153 -L 38.584684 432 -L 24.898908 405.140153 -L 14.095723 376.996894 -L 6.293491 347.878567 -L 1.577694 318.104197 -L 0 288 -L 1.577694 257.895803 -L 6.293491 228.121433 -L 14.095723 199.003106 -L 24.898908 170.859847 -L 38.584684 144 -L 55.003106 118.717847 -L 73.97429 95.290385 -L 95.290385 73.97429 -L 118.717847 55.003106 -L 144 38.584684 -L 170.859847 24.898908 -L 199.003106 14.095723 -L 228.121433 6.293491 -L 257.895803 1.577694 -L 288 0 -L 318.104197 1.577694 -L 347.878567 6.293491 -L 376.996894 14.095723 -L 405.140153 24.898908 -L 432 38.584684 -L 457.282153 55.003106 -L 480.709615 73.97429 -L 502.02571 95.290385 -L 520.996894 118.717847 -L 537.415316 144 -L 551.101092 170.859847 -L 561.904277 199.003106 -L 569.706509 228.121433 -L 574.422306 257.895803 -L 576 288 -L 576 288 + <path d="M 576 288 +L 574.422306 318.104197 +L 569.706509 347.878567 +L 561.904277 376.996894 +L 551.101092 405.140153 +L 537.415316 432 +L 520.996894 457.282153 +L 502.02571 480.709615 +L 480.709615 502.02571 +L 457.282153 520.996894 +L 432 537.415316 +L 405.140153 551.101092 +L 376.996894 561.904277 +L 347.878567 569.706509 +L 318.104197 574.422306 +L 288 576 +L 257.895803 574.422306 +L 228.121433 569.706509 +L 199.003106 561.904277 +L 170.859847 551.101092 +L 144 537.415316 +L 118.717847 520.996894 +L 95.290385 502.02571 +L 73.97429 480.709615 +L 55.003106 457.282153 +L 38.584684 432 +L 24.898908 405.140153 +L 14.095723 376.996894 +L 6.293491 347.878567 +L 1.577694 318.104197 +L 0 288 +L 1.577694 257.895803 +L 6.293491 228.121433 +L 14.095723 199.003106 +L 24.898908 170.859847 +L 38.584684 144 +L 55.003106 118.717847 +L 73.97429 95.290385 +L 95.290385 73.97429 +L 118.717847 55.003106 +L 144 38.584684 +L 170.859847 24.898908 +L 199.003106 14.095723 +L 228.121433 6.293491 +L 257.895803 1.577694 +L 288 0 +L 318.104197 1.577694 +L 347.878567 6.293491 +L 376.996894 14.095723 +L 405.140153 24.898908 +L 432 38.584684 +L 457.282153 55.003106 +L 480.709615 73.97429 +L 502.02571 95.290385 +L 520.996894 118.717847 +L 537.415316 144 +L 551.101092 170.859847 +L 561.904277 199.003106 +L 569.706509 228.121433 +L 574.422306 257.895803 +L 576 288 +L 576 288 " style="fill:#ffffff;"/> </g> <g id="PathCollection_1"> - <path clip-path="url(#p9f79fa544f)" d="M 203.761349 563.179244 -L 202.412957 562.754002 -L 200.470608 562.131786 -L 198.551685 561.500186 -L 196.840964 560.923021 -L 195.082213 560.299854 -L 193.428545 559.686272 -L 192.116937 559.147027 -L 190.602243 558.521237 -L 189.02197 557.881607 -L 187.024964 557.107796 -L 185.25523 556.399542 -L 183.693862 555.668177 -L 182.870585 555.22029 -L 181.820786 554.637621 -L 181.082972 554.163034 -L 180.564078 553.739195 -L 179.142597 552.978858 -L 178.531692 552.538005 -L 178.099744 552.144133 -L 178.817332 552.188642 -L 178.209506 551.674023 -L 178.180716 551.401682 -L 178.616909 551.30384 -L 178.441197 550.912662 -L 177.890795 550.371721 -L 178.373337 550.262661 -L 180.049467 550.877555 -L 180.867534 551.485474 -L 180.80956 551.9034 -L 181.157706 552.257956 -L 179.720671 551.827072 -L 179.213249 551.88512 -L 179.423145 552.234919 -L 179.702131 552.613252 -L 180.401805 553.101855 -L 181.857679 553.828143 -L 183.427697 554.537197 -L 185.228428 555.227267 -L 185.548188 555.549855 -L 185.022068 555.545459 -L 185.926169 556.061889 -L 186.740822 556.530255 -L 188.047868 557.192038 -L 189.502015 557.811324 -L 191.522233 558.560669 -L 193.262649 559.158568 -L 194.902728 559.66031 -L 196.831376 560.271163 -L 198.804326 560.944248 -L 200.536451 561.54654 -L 202.410978 562.175596 -L 204.174405 562.727071 -L 205.960085 563.300866 -L 209.921729 564.594219 -L 211.7008 565.123963 -L 213.402498 565.641212 -L 215.272236 566.171108 -L 217.400376 566.803144 -L 218.019911 566.999814 -L 210.89063 565.089529 + <path clip-path="url(#p9f79fa544f)" d="M 203.761349 563.179244 +L 202.412957 562.754002 +L 200.470608 562.131786 +L 198.551685 561.500186 +L 196.840964 560.923021 +L 195.082213 560.299854 +L 193.428545 559.686272 +L 192.116937 559.147027 +L 190.602243 558.521237 +L 189.02197 557.881607 +L 187.024964 557.107796 +L 185.25523 556.399542 +L 183.693862 555.668177 +L 182.870585 555.22029 +L 181.820786 554.637621 +L 181.082972 554.163034 +L 180.564078 553.739195 +L 179.142597 552.978858 +L 178.531692 552.538005 +L 178.099744 552.144133 +L 178.817332 552.188642 +L 178.209506 551.674023 +L 178.180716 551.401682 +L 178.616909 551.30384 +L 178.441197 550.912662 +L 177.890795 550.371721 +L 178.373337 550.262661 +L 180.049467 550.877555 +L 180.867534 551.485474 +L 180.80956 551.9034 +L 181.157706 552.257956 +L 179.720671 551.827072 +L 179.213249 551.88512 +L 179.423145 552.234919 +L 179.702131 552.613252 +L 180.401805 553.101855 +L 181.857679 553.828143 +L 183.427697 554.537197 +L 185.228428 555.227267 +L 185.548188 555.549855 +L 185.022068 555.545459 +L 185.926169 556.061889 +L 186.740822 556.530255 +L 188.047868 557.192038 +L 189.502015 557.811324 +L 191.522233 558.560669 +L 193.262649 559.158568 +L 194.902728 559.66031 +L 196.831376 560.271163 +L 198.804326 560.944248 +L 200.536451 561.54654 +L 202.410978 562.175596 +L 204.174405 562.727071 +L 205.960085 563.300866 +L 209.921729 564.594219 +L 211.7008 565.123963 +L 213.402498 565.641212 +L 215.272236 566.171108 +L 217.400376 566.803144 +L 218.019911 566.999814 +L 210.89063 565.089529 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 376.353079 562.076786 -L 377.353995 561.755526 -L 377.541962 561.695045 -L 377.541964 561.695044 -L 376.996894 561.904277 + <path clip-path="url(#p9f79fa544f)" d="M 376.353079 562.076786 +L 377.353995 561.755526 +L 377.541962 561.695045 +L 377.541964 561.695044 +L 376.996894 561.904277 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 381.674069 560.108877 -L 380.004093 560.632393 -L 377.872663 561.31785 -L 376.238963 561.893385 -L 377.005585 561.713332 -L 375.187036 562.316592 -L 373.586265 562.818152 -L 347.878567 569.706509 -L 318.104197 574.422306 -L 288 576 -L 261.298135 574.600615 -L 261.706333 574.592848 -L 262.291991 574.574744 -L 263.305936 574.594542 -L 264.393551 574.59807 -L 264.898451 574.537722 -L 263.52393 574.34369 -L 262.331865 574.147176 -L 261.4208 573.945035 -L 259.959874 573.886455 -L 258.77335 573.884714 -L 257.291603 573.795422 -L 256.008538 573.565279 -L 253.968487 573.101609 -L 253.727363 572.980051 -L 254.299892 572.892235 -L 255.173185 572.812497 -L 255.541837 572.661469 -L 255.9783 572.503576 -L 255.935346 572.247449 -L 256.937059 572.174674 -L 258.056744 572.128308 -L 258.623644 572.096486 -L 260.221563 572.132275 -L 261.579842 572.082345 -L 262.616291 571.971027 -L 263.615049 571.821477 -L 264.530272 571.661791 -L 265.740496 571.437503 -L 266.364434 571.168739 -L 267.169365 570.928634 -L 267.045768 570.566295 -L 265.32393 570.301662 -L 265.245549 569.900389 -L 265.760453 569.598678 -L 266.98771 569.426083 -L 268.297097 569.208091 -L 269.460206 568.894943 -L 270.250982 568.476866 -L 270.567236 567.945362 -L 271.46713 567.637908 -L 273.412714 567.736572 -L 274.429162 568.139847 -L 276.324749 568.20832 -L 276.15819 567.77122 -L 276.748571 567.306609 -L 278.553705 567.443115 -L 279.138648 567.893318 -L 281.067888 567.977046 -L 283.084817 567.77974 -L 285.087104 567.646404 -L 286.933155 567.720374 -L 287.637955 568.2061 -L 289.398789 567.813542 -L 291.066927 567.59884 -L 292.946479 567.425785 -L 294.817766 567.24794 -L 296.591615 566.944487 -L 298.5257 566.731214 -L 300.091273 566.441844 -L 301.33862 565.972066 -L 302.434127 566.28838 -L 304.291408 566.085861 -L 305.122242 566.672348 -L 305.746081 567.098889 -L 307.776423 566.82219 -L 308.908911 566.309514 -L 310.896427 565.918344 -L 313.01691 565.94768 -L 313.20715 566.415435 -L 315.027186 565.899611 -L 316.962946 565.698521 -L 318.954214 565.5929 -L 320.656112 565.567827 -L 322.277582 565.667277 -L 323.904979 565.687672 -L 324.065023 566.098342 -L 324.538921 566.440366 -L 326.550643 566.151535 -L 328.424535 566.030222 -L 330.147714 565.958879 -L 331.867381 565.860291 -L 333.661428 565.611919 -L 335.503598 565.380754 -L 337.456905 564.945729 -L 339.282164 564.631954 -L 341.037467 564.415327 -L 342.863721 563.967104 -L 345.092121 563.12905 -L 346.819913 562.585549 -L 347.978277 562.725536 -L 347.608982 563.20974 -L 348.216855 563.472514 -L 349.922283 563.262486 -L 349.90233 563.704426 -L 350.17997 563.995696 -L 352.259624 563.563366 -L 354.039051 562.901893 -L 355.809525 562.548949 -L 358.259049 561.937821 -L 360.021488 561.621682 -L 362.230915 561.187125 -L 363.980652 560.758602 -L 365.851791 560.287179 -L 367.609754 559.844528 -L 368.435769 559.921349 -L 370.811231 559.230581 -L 372.596461 558.68975 -L 373.722683 558.600982 -L 375.591523 558.092897 -L 377.100096 557.457597 -L 379.145825 556.860587 -L 380.882265 556.396828 -L 382.685003 555.972334 -L 384.086402 555.696692 -L 384.906152 555.674218 -L 385.61499 555.713284 -L 385.456541 556.09021 -L 383.892914 556.844016 -L 383.52261 557.299837 -L 383.05999 557.687321 -L 383.770394 557.716402 -L 383.297724 558.08934 -L 382.937394 558.435007 -L 383.547189 558.395112 -L 385.052831 557.944921 -L 387.499299 557.105282 -L 387.373279 557.338272 -L 387.651475 557.393929 -L 387.863706 557.454663 -L 388.238444 557.441112 -L 388.933106 557.288269 -L 387.991878 557.683698 + <path clip-path="url(#p9f79fa544f)" d="M 381.674069 560.108877 +L 380.004093 560.632393 +L 377.872663 561.31785 +L 376.238963 561.893385 +L 377.005585 561.713332 +L 375.187036 562.316592 +L 373.586265 562.818152 +L 347.878567 569.706509 +L 318.104197 574.422306 +L 288 576 +L 261.298135 574.600615 +L 261.706333 574.592848 +L 262.291991 574.574744 +L 263.305936 574.594542 +L 264.393551 574.59807 +L 264.898451 574.537722 +L 263.52393 574.34369 +L 262.331865 574.147176 +L 261.4208 573.945035 +L 259.959874 573.886455 +L 258.77335 573.884714 +L 257.291603 573.795422 +L 256.008538 573.565279 +L 253.968487 573.101609 +L 253.727363 572.980051 +L 254.299892 572.892235 +L 255.173185 572.812497 +L 255.541837 572.661469 +L 255.9783 572.503576 +L 255.935346 572.247449 +L 256.937059 572.174674 +L 258.056744 572.128308 +L 258.623644 572.096486 +L 260.221563 572.132275 +L 261.579842 572.082345 +L 262.616291 571.971027 +L 263.615049 571.821477 +L 264.530272 571.661791 +L 265.740496 571.437503 +L 266.364434 571.168739 +L 267.169365 570.928634 +L 267.045768 570.566295 +L 265.32393 570.301662 +L 265.245549 569.900389 +L 265.760453 569.598678 +L 266.98771 569.426083 +L 268.297097 569.208091 +L 269.460206 568.894943 +L 270.250982 568.476866 +L 270.567236 567.945362 +L 271.46713 567.637908 +L 273.412714 567.736572 +L 274.429162 568.139847 +L 276.324749 568.20832 +L 276.15819 567.77122 +L 276.748571 567.306609 +L 278.553705 567.443115 +L 279.138648 567.893318 +L 281.067888 567.977046 +L 283.084817 567.77974 +L 285.087104 567.646404 +L 286.933155 567.720374 +L 287.637955 568.2061 +L 289.398789 567.813542 +L 291.066927 567.59884 +L 292.946479 567.425785 +L 294.817766 567.24794 +L 296.591615 566.944487 +L 298.5257 566.731214 +L 300.091273 566.441844 +L 301.33862 565.972066 +L 302.434127 566.28838 +L 304.291408 566.085861 +L 305.122242 566.672348 +L 305.746081 567.098889 +L 307.776423 566.82219 +L 308.908911 566.309514 +L 310.896427 565.918344 +L 313.01691 565.94768 +L 313.20715 566.415435 +L 315.027186 565.899611 +L 316.962946 565.698521 +L 318.954214 565.5929 +L 320.656112 565.567827 +L 322.277582 565.667277 +L 323.904979 565.687672 +L 324.065023 566.098342 +L 324.538921 566.440366 +L 326.550643 566.151535 +L 328.424535 566.030222 +L 330.147714 565.958879 +L 331.867381 565.860291 +L 333.661428 565.611919 +L 335.503598 565.380754 +L 337.456905 564.945729 +L 339.282164 564.631954 +L 341.037467 564.415327 +L 342.863721 563.967104 +L 345.092121 563.12905 +L 346.819913 562.585549 +L 347.978277 562.725536 +L 347.608982 563.20974 +L 348.216855 563.472514 +L 349.922283 563.262486 +L 349.90233 563.704426 +L 350.17997 563.995696 +L 352.259624 563.563366 +L 354.039051 562.901893 +L 355.809525 562.548949 +L 358.259049 561.937821 +L 360.021488 561.621682 +L 362.230915 561.187125 +L 363.980652 560.758602 +L 365.851791 560.287179 +L 367.609754 559.844528 +L 368.435769 559.921349 +L 370.811231 559.230581 +L 372.596461 558.68975 +L 373.722683 558.600982 +L 375.591523 558.092897 +L 377.100096 557.457597 +L 379.145825 556.860587 +L 380.882265 556.396828 +L 382.685003 555.972334 +L 384.086402 555.696692 +L 384.906152 555.674218 +L 385.61499 555.713284 +L 385.456541 556.09021 +L 383.892914 556.844016 +L 383.52261 557.299837 +L 383.05999 557.687321 +L 383.770394 557.716402 +L 383.297724 558.08934 +L 382.937394 558.435007 +L 383.547189 558.395112 +L 385.052831 557.944921 +L 387.499299 557.105282 +L 387.373279 557.338272 +L 387.651475 557.393929 +L 387.863706 557.454663 +L 388.238444 557.441112 +L 388.933106 557.288269 +L 387.991878 557.683698 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 259.775571 574.52082 -L 259.735748 574.505457 -L 260.456438 574.556503 -L 260.116004 574.538662 + <path clip-path="url(#p9f79fa544f)" d="M 259.775571 574.52082 +L 259.735748 574.505457 +L 260.456438 574.556503 +L 260.116004 574.538662 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 257.594847 574.374639 -L 258.274705 574.442163 -L 258.274706 574.442163 -L 257.895803 574.422306 + <path clip-path="url(#p9f79fa544f)" d="M 257.594847 574.374639 +L 258.274705 574.442163 +L 258.274706 574.442163 +L 257.895803 574.422306 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 130.756911 525.277015 -L 134.497835 527.260979 -L 137.106714 528.274087 -L 138.432258 529.534114 -L 137.511949 529.45278 -L 135.594709 528.372915 -L 136.760693 529.398681 -L 137.023879 530.040942 -L 135.468969 529.486238 -L 133.580475 528.498584 -L 132.012529 527.85461 -L 128.686092 526.00221 -L 125.70329 524.23521 -L 120.220077 520.654092 -L 121.708433 521.477161 -L 125.508318 523.755244 -L 128.121758 525.099853 -L 126.865819 523.924505 -L 124.593572 522.035091 -L 124.012205 521.062022 -L 125.231007 521.583066 -L 125.231566 521.583442 -L 127.387694 523.019829 + <path clip-path="url(#p9f79fa544f)" d="M 130.756911 525.277015 +L 134.497835 527.260979 +L 137.106714 528.274087 +L 138.432258 529.534114 +L 137.511949 529.45278 +L 135.594709 528.372915 +L 136.760693 529.398681 +L 137.023879 530.040942 +L 135.468969 529.486238 +L 133.580475 528.498584 +L 132.012529 527.85461 +L 128.686092 526.00221 +L 125.70329 524.23521 +L 120.220077 520.654092 +L 121.708433 521.477161 +L 125.508318 523.755244 +L 128.121758 525.099853 +L 126.865819 523.924505 +L 124.593572 522.035091 +L 124.012205 521.062022 +L 125.231007 521.583066 +L 125.231566 521.583442 +L 127.387694 523.019829 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 133.713214 519.507549 -L 136.539241 521.020494 -L 137.212854 521.973195 -L 136.062681 522.532379 -L 134.162445 521.410431 -L 134.409809 522.519547 -L 132.09994 521.092384 -L 131.883538 519.609958 -L 134.08068 520.548505 + <path clip-path="url(#p9f79fa544f)" d="M 133.713214 519.507549 +L 136.539241 521.020494 +L 137.212854 521.973195 +L 136.062681 522.532379 +L 134.162445 521.410431 +L 134.409809 522.519547 +L 132.09994 521.092384 +L 131.883538 519.609958 +L 134.08068 520.548505 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 463.316589 512.324218 -L 461.337923 512.934022 -L 463.205304 511.273579 -L 464.840825 509.929292 -L 465.643941 509.258249 -L 465.286733 510.08671 -L 465.904089 510.227941 -L 465.261143 510.819689 + <path clip-path="url(#p9f79fa544f)" d="M 463.316589 512.324218 +L 461.337923 512.934022 +L 463.205304 511.273579 +L 464.840825 509.929292 +L 465.643941 509.258249 +L 465.286733 510.08671 +L 465.904089 510.227941 +L 465.261143 510.819689 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 502.654347 370.915332 -L 502.021775 376.621295 -L 502.356327 378.765425 -L 501.553979 381.076495 -L 500.696895 382.528464 -L 500.252229 379.858054 -L 499.359547 381.323793 -L 499.167097 384.768959 -L 498.424347 386.797091 -L 497.306703 387.972242 -L 496.135825 391.964073 -L 493.499549 397.516359 -L 490.196978 404.026921 -L 485.675191 412.842772 -L 482.45591 419.201268 -L 479.285383 424.481671 -L 476.378264 425.760671 -L 472.962826 427.866647 -L 471.509862 426.90773 -L 469.466539 425.554305 -L 469.327981 423.300062 -L 470.36287 419.377516 -L 470.269024 415.904991 -L 470.905565 412.687326 -L 472.43347 409.367914 -L 474.211542 408.449845 -L 474.636284 406.930682 -L 477.178993 403.320521 -L 478.240658 400.360264 -L 478.000259 398.237157 -L 478.058504 395.363334 -L 478.770974 391.084153 -L 480.552231 388.356963 -L 481.648463 385.329319 -L 483.395034 385.00786 -L 485.49519 383.872307 -L 486.923781 382.904468 -L 488.410449 382.707425 -L 490.863336 379.828725 -L 494.159557 376.639512 -L 495.60461 374.141127 -L 495.541964 372.136664 -L 496.821046 372.57851 -L 499.212841 369.064828 -L 499.776158 366.160939 -L 501.203246 363.892423 -L 501.971537 365.853679 -L 502.455979 367.819151 + <path clip-path="url(#p9f79fa544f)" d="M 502.654347 370.915332 +L 502.021775 376.621295 +L 502.356327 378.765425 +L 501.553979 381.076495 +L 500.696895 382.528464 +L 500.252229 379.858054 +L 499.359547 381.323793 +L 499.167097 384.768959 +L 498.424347 386.797091 +L 497.306703 387.972242 +L 496.135825 391.964073 +L 493.499549 397.516359 +L 490.196978 404.026921 +L 485.675191 412.842772 +L 482.45591 419.201268 +L 479.285383 424.481671 +L 476.378264 425.760671 +L 472.962826 427.866647 +L 471.509862 426.90773 +L 469.466539 425.554305 +L 469.327981 423.300062 +L 470.36287 419.377516 +L 470.269024 415.904991 +L 470.905565 412.687326 +L 472.43347 409.367914 +L 474.211542 408.449845 +L 474.636284 406.930682 +L 477.178993 403.320521 +L 478.240658 400.360264 +L 478.000259 398.237157 +L 478.058504 395.363334 +L 478.770974 391.084153 +L 480.552231 388.356963 +L 481.648463 385.329319 +L 483.395034 385.00786 +L 485.49519 383.872307 +L 486.923781 382.904468 +L 488.410449 382.707425 +L 490.863336 379.828725 +L 494.159557 376.639512 +L 495.60461 374.141127 +L 495.541964 372.136664 +L 496.821046 372.57851 +L 499.212841 369.064828 +L 499.776158 366.160939 +L 501.203246 363.892423 +L 501.971537 365.853679 +L 502.455979 367.819151 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 40.174361 249.641582 -L 38.108344 249.87188 -L 37.757742 249.356403 -L 38.651844 248.101317 -L 38.925312 246.13425 -L 40.392314 245.706958 -L 40.865862 245.959497 + <path clip-path="url(#p9f79fa544f)" d="M 40.174361 249.641582 +L 38.108344 249.87188 +L 37.757742 249.356403 +L 38.651844 248.101317 +L 38.925312 246.13425 +L 40.392314 245.706958 +L 40.865862 245.959497 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 38.899648 208.108035 -L 38.037433 209.224956 -L 36.594794 208.909394 -L 35.448152 208.855328 -L 35.958445 206.774347 -L 36.434919 206.126589 -L 37.969516 206.466805 -L 38.831473 207.084189 + <path clip-path="url(#p9f79fa544f)" d="M 38.899648 208.108035 +L 38.037433 209.224956 +L 36.594794 208.909394 +L 35.448152 208.855328 +L 35.958445 206.774347 +L 36.434919 206.126589 +L 37.969516 206.466805 +L 38.831473 207.084189 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 29.568545 197.544248 -L 30.571766 198.631626 -L 31.033438 197.869502 -L 32.215244 198.195932 -L 32.737987 199.651977 -L 33.202927 199.634511 -L 32.941189 201.397484 -L 33.881399 201.514088 -L 33.36734 202.926427 -L 34.068888 203.2714 -L 34.370528 205.219209 -L 33.120289 207.030221 -L 32.624776 205.800879 -L 31.791962 205.82356 -L 31.320807 205.466831 -L 30.75607 206.271881 -L 30.047751 206.417409 -L 30.167714 205.193313 -L 29.43519 205.752232 -L 27.814656 208.886274 -L 27.654221 208.019016 -L 27.99968 206.61722 -L 27.274263 205.538192 -L 26.496811 205.683505 -L 25.760692 205.093647 -L 24.852426 205.803362 -L 24.625885 204.095092 -L 25.234067 202.587563 -L 26.235481 203.619141 -L 27.160544 204.303464 -L 28.015728 203.379953 -L 28.037181 201.128434 -L 28.653714 199.309781 -L 28.036126 198.318044 -L 28.792083 197.094057 + <path clip-path="url(#p9f79fa544f)" d="M 29.568545 197.544248 +L 30.571766 198.631626 +L 31.033438 197.869502 +L 32.215244 198.195932 +L 32.737987 199.651977 +L 33.202927 199.634511 +L 32.941189 201.397484 +L 33.881399 201.514088 +L 33.36734 202.926427 +L 34.068888 203.2714 +L 34.370528 205.219209 +L 33.120289 207.030221 +L 32.624776 205.800879 +L 31.791962 205.82356 +L 31.320807 205.466831 +L 30.75607 206.271881 +L 30.047751 206.417409 +L 30.167714 205.193313 +L 29.43519 205.752232 +L 27.814656 208.886274 +L 27.654221 208.019016 +L 27.99968 206.61722 +L 27.274263 205.538192 +L 26.496811 205.683505 +L 25.760692 205.093647 +L 24.852426 205.803362 +L 24.625885 204.095092 +L 25.234067 202.587563 +L 26.235481 203.619141 +L 27.160544 204.303464 +L 28.015728 203.379953 +L 28.037181 201.128434 +L 28.653714 199.309781 +L 28.036126 198.318044 +L 28.792083 197.094057 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 26.729806 181.125911 -L 26.37229 182.988142 -L 27.426773 182.832688 -L 27.32375 184.069007 -L 27.036248 187.30301 -L 26.893081 189.669021 -L 27.281185 189.734997 -L 27.604406 190.931863 -L 27.031017 192.231108 -L 27.839534 192.717819 -L 28.093206 194.953311 -L 27.550471 196.017052 -L 26.507577 196.355498 -L 25.618014 196.323014 -L 24.960553 195.672333 -L 23.280405 195.559465 -L 24.911533 193.187456 -L 24.936386 191.797594 -L 24.412758 191.224095 -L 24.590488 189.72595 -L 25.391067 186.95649 -L 24.816477 186.913001 -L 24.531857 185.284107 -L 24.686003 184.183819 -L 24.043993 182.938213 -L 24.186178 181.889849 -L 24.905206 180.878478 -L 24.39268 180.247843 -L 23.004245 182.374539 -L 22.766341 182.272401 -L 22.238754 183.351261 -L 21.805828 183.661003 -L 21.800144 183.045718 -L 22.607309 181.820692 -L 23.393984 180.23154 -L 24.041579 179.381975 -L 24.686963 178.683794 -L 25.30728 178.543623 -L 25.670658 178.131963 -L 26.097931 178.806543 -L 26.643423 179.171542 + <path clip-path="url(#p9f79fa544f)" d="M 26.729806 181.125911 +L 26.37229 182.988142 +L 27.426773 182.832688 +L 27.32375 184.069007 +L 27.036248 187.30301 +L 26.893081 189.669021 +L 27.281185 189.734997 +L 27.604406 190.931863 +L 27.031017 192.231108 +L 27.839534 192.717819 +L 28.093206 194.953311 +L 27.550471 196.017052 +L 26.507577 196.355498 +L 25.618014 196.323014 +L 24.960553 195.672333 +L 23.280405 195.559465 +L 24.911533 193.187456 +L 24.936386 191.797594 +L 24.412758 191.224095 +L 24.590488 189.72595 +L 25.391067 186.95649 +L 24.816477 186.913001 +L 24.531857 185.284107 +L 24.686003 184.183819 +L 24.043993 182.938213 +L 24.186178 181.889849 +L 24.905206 180.878478 +L 24.39268 180.247843 +L 23.004245 182.374539 +L 22.766341 182.272401 +L 22.238754 183.351261 +L 21.805828 183.661003 +L 21.800144 183.045718 +L 22.607309 181.820692 +L 23.393984 180.23154 +L 24.041579 179.381975 +L 24.686963 178.683794 +L 25.30728 178.543623 +L 25.670658 178.131963 +L 26.097931 178.806543 +L 26.643423 179.171542 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 420.776861 137.483202 -L 419.188536 139.445219 -L 419.739348 140.236487 -L 419.975721 140.576729 -L 417.088887 142.535789 -L 415.185355 142.069306 -L 413.75351 140.380053 -L 415.343922 140.130073 -L 415.579681 140.092775 -L 415.697739 139.027337 -L 418.186292 138.943988 + <path clip-path="url(#p9f79fa544f)" d="M 420.776861 137.483202 +L 419.188536 139.445219 +L 419.739348 140.236487 +L 419.975721 140.576729 +L 417.088887 142.535789 +L 415.185355 142.069306 +L 413.75351 140.380053 +L 415.343922 140.130073 +L 415.579681 140.092775 +L 415.697739 139.027337 +L 418.186292 138.943988 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 382.002534 139.221501 -L 384.445774 140.593852 -L 387.275591 140.232283 -L 390.119056 140.424305 -L 390.249139 141.179183 -L 392.107117 140.575642 -L 392.023556 141.869951 -L 386.772262 142.460197 -L 386.610726 141.744229 -L 381.80549 141.079775 + <path clip-path="url(#p9f79fa544f)" d="M 382.002534 139.221501 +L 384.445774 140.593852 +L 387.275591 140.232283 +L 390.119056 140.424305 +L 390.249139 141.179183 +L 392.107117 140.575642 +L 392.023556 141.869951 +L 386.772262 142.460197 +L 386.610726 141.744229 +L 381.80549 141.079775 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 348.535442 129.450208 -L 347.798829 132.799888 -L 348.624215 134.099834 -L 348.216569 136.309387 -L 344.95163 134.771881 -L 342.893469 134.35629 -L 337.110707 132.311468 -L 337.309899 130.13663 -L 341.882923 130.429159 -L 345.711213 129.884827 + <path clip-path="url(#p9f79fa544f)" d="M 348.535442 129.450208 +L 347.798829 132.799888 +L 348.624215 134.099834 +L 348.216569 136.309387 +L 344.95163 134.771881 +L 342.893469 134.35629 +L 337.110707 132.311468 +L 337.309899 130.13663 +L 341.882923 130.429159 +L 345.711213 129.884827 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 322.677962 117.619901 -L 325.313294 120.476754 -L 325.499539 125.935993 -L 323.719659 125.69926 -L 322.313288 127.109897 -L 320.725889 126.025267 -L 320.005514 121.049675 -L 318.874797 118.727577 -L 320.965191 118.904332 + <path clip-path="url(#p9f79fa544f)" d="M 322.677962 117.619901 +L 325.313294 120.476754 +L 325.499539 125.935993 +L 323.719659 125.69926 +L 322.313288 127.109897 +L 320.725889 126.025267 +L 320.005514 121.049675 +L 318.874797 118.727577 +L 320.965191 118.904332 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 323.460572 113.80555 -L 322.661187 116.930705 -L 320.866414 116.131858 -L 319.670159 113.441323 -L 320.220719 111.949711 -L 322.359818 110.402232 + <path clip-path="url(#p9f79fa544f)" d="M 323.460572 113.80555 +L 322.661187 116.930705 +L 320.866414 116.131858 +L 319.670159 113.441323 +L 320.220719 111.949711 +L 322.359818 110.402232 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 110.491213 87.370887 -L 111.182119 88.047987 -L 112.75471 88.198156 -L 110.642231 89.54576 -L 109.839288 89.681856 -L 109.247827 87.805963 -L 109.973799 86.539115 -L 111.557835 85.542 + <path clip-path="url(#p9f79fa544f)" d="M 110.491213 87.370887 +L 111.182119 88.047987 +L 112.75471 88.198156 +L 110.642231 89.54576 +L 109.839288 89.681856 +L 109.247827 87.805963 +L 109.973799 86.539115 +L 111.557835 85.542 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 121.82218 78.887657 -L 121.01105 78.825717 -L 120.139341 77.423366 -L 120.441691 75.585333 -L 121.218084 75.389781 -L 122.257232 76.569779 -L 122.39218 78.251118 + <path clip-path="url(#p9f79fa544f)" d="M 121.82218 78.887657 +L 121.01105 78.825717 +L 120.139341 77.423366 +L 120.441691 75.585333 +L 121.218084 75.389781 +L 122.257232 76.569779 +L 122.39218 78.251118 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 136.490517 74.883035 -L 132.497669 77.692014 -L 134.747155 76.694959 -L 135.282574 77.580864 -L 133.535044 78.69257 -L 134.320776 79.844728 -L 135.951622 79.111627 -L 136.920718 80.403888 -L 134.081026 82.887004 -L 135.984839 82.450059 -L 134.646653 84.347644 -L 133.434819 86.63143 -L 129.981933 89.701877 -L 128.939904 89.727809 -L 128.160925 88.884686 -L 131.039745 85.999954 -L 130.86959 85.480919 -L 125.972424 88.304306 -L 124.922846 88.031687 -L 127.714673 86.520323 -L 126.626714 85.419568 -L 124.446612 85.366319 -L 121.085324 84.770124 -L 121.768615 83.685353 -L 123.972614 82.60496 -L 124.100228 81.547253 -L 127.489125 79.665668 -L 134.454375 74.493917 -L 137.386901 72.738456 -L 140.021842 71.794672 -L 140.69681 72.043645 -L 139.475309 72.899053 + <path clip-path="url(#p9f79fa544f)" d="M 136.490517 74.883035 +L 132.497669 77.692014 +L 134.747155 76.694959 +L 135.282574 77.580864 +L 133.535044 78.69257 +L 134.320776 79.844728 +L 135.951622 79.111627 +L 136.920718 80.403888 +L 134.081026 82.887004 +L 135.984839 82.450059 +L 134.646653 84.347644 +L 133.434819 86.63143 +L 129.981933 89.701877 +L 128.939904 89.727809 +L 128.160925 88.884686 +L 131.039745 85.999954 +L 130.86959 85.480919 +L 125.972424 88.304306 +L 124.922846 88.031687 +L 127.714673 86.520323 +L 126.626714 85.419568 +L 124.446612 85.366319 +L 121.085324 84.770124 +L 121.768615 83.685353 +L 123.972614 82.60496 +L 124.100228 81.547253 +L 127.489125 79.665668 +L 134.454375 74.493917 +L 137.386901 72.738456 +L 140.021842 71.794672 +L 140.69681 72.043645 +L 139.475309 72.899053 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 267.161778 76.37076 -L 261.408434 78.331728 -L 257.156819 77.749923 -L 260.302878 74.234553 -L 259.429759 70.822135 -L 263.874051 68.311636 -L 266.303362 66.827163 -L 268.713598 66.720591 -L 271.521933 68.75172 -L 269.665571 70.992272 -L 269.848115 73.374387 + <path clip-path="url(#p9f79fa544f)" d="M 267.161778 76.37076 +L 261.408434 78.331728 +L 257.156819 77.749923 +L 260.302878 74.234553 +L 259.429759 70.822135 +L 263.874051 68.311636 +L 266.303362 66.827163 +L 268.713598 66.720591 +L 271.521933 68.75172 +L 269.665571 70.992272 +L 269.848115 73.374387 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 323.734852 65.072312 -L 322.771282 67.70357 -L 319.35486 65.938405 -L 318.637699 64.622596 -L 322.403537 63.502761 + <path clip-path="url(#p9f79fa544f)" d="M 323.734852 65.072312 +L 322.771282 67.70357 +L 319.35486 65.938405 +L 318.637699 64.622596 +L 322.403537 63.502761 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 280.141655 56.065946 -L 277.02269 59.315768 -L 279.79597 58.912686 -L 282.73624 58.939811 -L 281.902689 61.443176 -L 279.231375 64.241843 -L 282.127031 64.455392 -L 284.755551 68.590642 -L 286.742348 69.116817 -L 288.555361 72.882352 -L 289.424007 74.207244 -L 293.116671 74.843926 -L 292.816281 77.019826 -L 291.264988 78.029066 -L 292.557263 79.815073 -L 289.749641 81.647049 -L 285.49683 81.613849 -L 280.041215 82.566676 -L 278.591508 81.868226 -L 276.375233 83.509182 -L 273.442875 83.089516 -L 271.062729 84.42582 -L 269.429269 83.702793 -L 274.44063 80.051714 -L 277.303481 79.318493 -L 272.454893 78.708001 -L 271.719213 77.332241 -L 275.033067 76.296198 -L 273.533625 74.458605 -L 274.319066 72.267555 -L 278.738554 72.595391 -L 279.299491 70.671449 -L 277.441064 68.59671 -L 273.977634 68.002146 -L 273.388061 67.121566 -L 274.582305 65.694624 -L 273.750186 64.809896 -L 272.044102 66.30708 -L 272.271393 63.244756 -L 271.098644 61.638424 -L 272.534095 58.471692 -L 274.907244 56.048852 -L 276.964936 56.299614 + <path clip-path="url(#p9f79fa544f)" d="M 280.141655 56.065946 +L 277.02269 59.315768 +L 279.79597 58.912686 +L 282.73624 58.939811 +L 281.902689 61.443176 +L 279.231375 64.241843 +L 282.127031 64.455392 +L 284.755551 68.590642 +L 286.742348 69.116817 +L 288.555361 72.882352 +L 289.424007 74.207244 +L 293.116671 74.843926 +L 292.816281 77.019826 +L 291.264988 78.029066 +L 292.557263 79.815073 +L 289.749641 81.647049 +L 285.49683 81.613849 +L 280.041215 82.566676 +L 278.591508 81.868226 +L 276.375233 83.509182 +L 273.442875 83.089516 +L 271.062729 84.42582 +L 269.429269 83.702793 +L 274.44063 80.051714 +L 277.303481 79.318493 +L 272.454893 78.708001 +L 271.719213 77.332241 +L 275.033067 76.296198 +L 273.533625 74.458605 +L 274.319066 72.267555 +L 278.738554 72.595391 +L 279.299491 70.671449 +L 277.441064 68.59671 +L 273.977634 68.002146 +L 273.388061 67.121566 +L 274.582305 65.694624 +L 273.750186 64.809896 +L 272.044102 66.30708 +L 272.271393 63.244756 +L 271.098644 61.638424 +L 272.534095 58.471692 +L 274.907244 56.048852 +L 276.964936 56.299614 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 259.178337 34.68425 -L 257.972799 36.235985 -L 259.495377 37.967983 -L 255.939369 39.830135 -L 248.974044 41.436442 -L 246.886986 41.858094 -L 244.326541 41.390471 -L 239.080952 40.378247 -L 241.833216 39.324345 -L 238.379912 37.938798 -L 242.187893 37.586124 -L 242.610513 36.859012 -L 239.011521 36.14006 -L 241.492046 34.621236 -L 244.573776 34.372864 -L 246.398305 36.104435 -L 250.167811 34.882424 -L 252.184498 35.633207 -L 256.056755 34.438478 + <path clip-path="url(#p9f79fa544f)" d="M 259.178337 34.68425 +L 257.972799 36.235985 +L 259.495377 37.967983 +L 255.939369 39.830135 +L 248.974044 41.436442 +L 246.886986 41.858094 +L 244.326541 41.390471 +L 239.080952 40.378247 +L 241.833216 39.324345 +L 238.379912 37.938798 +L 242.187893 37.586124 +L 242.610513 36.859012 +L 239.011521 36.14006 +L 241.492046 34.621236 +L 244.573776 34.372864 +L 246.398305 36.104435 +L 250.167811 34.882424 +L 252.184498 35.633207 +L 256.056755 34.438478 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 192.321152 16.660685 -L 195.739182 15.507324 -L 198.991197 14.551407 -L 198.063667 14.975955 -L 195.868058 15.805049 -L 190.435608 17.843986 -L 188.17003 18.547304 -L 187.132945 19.182365 -L 182.335032 21.013819 -L 186.013759 19.809512 -L 183.04789 21.162315 -L 179.569569 22.500095 -L 176.50786 23.963261 -L 179.971408 22.739857 -L 184.088817 21.238511 -L 189.242835 19.272284 -L 189.105495 19.620954 -L 188.713874 20.106045 -L 186.734255 21.213101 -L 184.449789 22.147621 -L 181.783339 23.043591 -L 179.512801 24.200236 -L 177.245108 25.146475 -L 173.528802 26.229913 -L 172.530652 26.303332 -L 173.740054 25.509494 -L 171.392324 26.481592 -L 167.418284 28.06989 -L 165.397732 28.950261 -L 162.239951 30.202839 -L 161.89581 30.074392 -L 159.999784 30.854093 -L 157.0836 32.30094 -L 156.586207 32.353377 -L 153.105588 33.969664 -L 148.311855 36.408976 -L 144.947137 38.246069 -L 141.15368 40.433105 -L 144 38.584684 -L 170.859847 24.898908 -L 180.781273 21.090429 -L 180.769597 21.098912 -L 185.393936 19.363646 -L 187.249451 18.607529 -L 187.249454 18.607528 + <path clip-path="url(#p9f79fa544f)" d="M 192.321152 16.660685 +L 195.739182 15.507324 +L 198.991197 14.551407 +L 198.063667 14.975955 +L 195.868058 15.805049 +L 190.435608 17.843986 +L 188.17003 18.547304 +L 187.132945 19.182365 +L 182.335032 21.013819 +L 186.013759 19.809512 +L 183.04789 21.162315 +L 179.569569 22.500095 +L 176.50786 23.963261 +L 179.971408 22.739857 +L 184.088817 21.238511 +L 189.242835 19.272284 +L 189.105495 19.620954 +L 188.713874 20.106045 +L 186.734255 21.213101 +L 184.449789 22.147621 +L 181.783339 23.043591 +L 179.512801 24.200236 +L 177.245108 25.146475 +L 173.528802 26.229913 +L 172.530652 26.303332 +L 173.740054 25.509494 +L 171.392324 26.481592 +L 167.418284 28.06989 +L 165.397732 28.950261 +L 162.239951 30.202839 +L 161.89581 30.074392 +L 159.999784 30.854093 +L 157.0836 32.30094 +L 156.586207 32.353377 +L 153.105588 33.969664 +L 148.311855 36.408976 +L 144.947137 38.246069 +L 141.15368 40.433105 +L 144 38.584684 +L 170.859847 24.898908 +L 180.781273 21.090429 +L 180.769597 21.098912 +L 185.393936 19.363646 +L 187.249451 18.607529 +L 187.249454 18.607528 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 10.963487 210.69277 -L 10.912767 210.977623 -L 10.875742 211.209563 -L 11.152642 210.454176 -L 11.226543 210.505237 -L 11.13371 210.937808 -L 11.235107 210.763836 -L 11.224053 211.394577 -L 11.389577 211.462787 -L 11.641916 211.064455 -L 11.835202 210.573582 -L 11.875301 210.957323 -L 11.87132 211.385892 -L 12.007067 211.381758 -L 12.208577 211.047251 -L 12.219987 211.907823 -L 12.263912 212.081695 -L 12.159347 213.112141 -L 12.007414 214.318088 -L 12.001568 215.212494 -L 11.792682 216.655832 -L 11.619556 217.085464 -L 11.306081 218.147627 -L 10.907413 219.969583 -L 10.360926 221.530696 -L 9.823023 223.447232 -L 9.297138 225.585274 -L 9.037251 226.86853 -L 8.55909 229.08377 -L 8.369744 229.50019 -L 7.870834 231.559234 -L 7.639147 232.885529 -L 7.274676 234.055859 -L 7.036147 235.405294 -L 6.961908 236.279318 -L 6.600544 239.066645 -L 6.471221 241.219072 -L 6.401928 243.530364 -L 6.346986 245.460064 -L 6.166323 246.500457 -L 6.406776 246.894659 -L 6.537297 246.531619 -L 6.547999 247.867688 -L 6.977072 247.720092 -L 7.512821 246.649808 -L 8.184624 245.909729 -L 8.729069 244.574113 -L 9.188963 245.099485 -L 9.07385 245.570054 -L 9.584064 245.975996 -L 9.894549 247.02064 -L 10.000626 248.650277 -L 10.19602 250.181197 -L 10.745986 250.557205 -L 12.151086 247.408183 -L 12.733744 247.053314 -L 13.014872 245.418208 -L 13.970163 241.314522 -L 15.118201 239.261132 -L 15.96189 239.428643 -L 16.264237 238.433158 -L 17.261448 239.167465 -L 18.876494 236.999502 -L 19.674862 236.056993 -L 20.896448 233.883074 -L 21.38342 234.328112 -L 21.531809 235.729353 -L 20.892806 237.310318 -L 20.620797 238.463862 -L 19.599643 238.801121 -L 19.681782 241.17548 -L 19.19921 243.757257 -L 18.030352 246.452983 -L 17.990237 250.544947 -L 18.718782 250.398917 -L 19.645912 246.904237 -L 19.442835 245.034235 -L 20.026075 241.267113 -L 22.421628 239.764824 -L 22.639544 237.382403 -L 23.535897 235.96927 -L 23.468427 239.579077 -L 24.678249 239.940812 -L 25.344183 242.958327 -L 25.128632 244.612513 -L 26.801549 245.013948 -L 28.971627 244.927532 -L 29.753686 247.372368 -L 31.237322 248.285489 -L 32.679209 246.953913 -L 32.911809 245.710066 -L 35.673218 245.88953 -L 38.419561 246.277713 -L 36.228292 247.419096 -L 36.634844 249.892877 -L 38.437342 250.571185 -L 39.884571 253.303621 -L 39.72266 257.352133 -L 41.016321 257.435886 -L 41.840187 258.758198 -L 43.275132 260.837998 -L 44.498198 264.321211 -L 44.299013 266.904432 -L 45.275992 267.162828 -L 46.464485 269.799829 -L 47.371258 271.687018 -L 50.587646 273.124812 -L 50.965623 272.261266 -L 53.29 272.189932 -L 56.299274 273.917272 -L 57.25841 274.587875 -L 59.370824 276.030234 -L 62.317038 280.625963 -L 62.707824 282.736341 -L 63.784895 282.616258 -L 64.413981 285.47342 -L 65.864185 294.434601 -L 67.562309 295.441792 -L 67.584825 298.910584 -L 65.135773 302.781602 -L 66.12804 304.396451 -L 71.8996 305.771605 -L 72.068077 310.798636 -L 74.573118 307.764296 -L 78.888033 309.966673 -L 84.761213 313.540957 -L 86.572013 316.618144 -L 86.066722 319.330151 -L 90.219731 318.14638 -L 97.512995 321.358699 -L 103.190402 321.590782 -L 109.08662 326.121165 -L 114.397163 332.029077 -L 117.55972 333.660892 -L 121.032611 334.079062 -L 122.595651 335.732982 -L 124.374932 342.109231 -L 125.267693 345.130331 -L 124.319633 353.141137 -L 122.570176 356.202158 -L 117.700346 362.608079 -L 115.901105 367.908057 -L 113.627772 371.883941 -L 112.684207 371.911509 -L 112.158391 375.361986 -L 113.930642 384.291693 -L 114.26724 391.472255 -L 114.504003 394.524713 -L 113.727248 396.277883 -L 114.470165 402.417045 -L 112.289979 408.128999 -L 112.928835 412.77324 -L 110.64276 414.51204 -L 110.609118 417.143441 -L 106.900812 416.836534 -L 102.185409 418.118937 -L 100.523404 419.905405 -L 97.371636 420.884327 -L 94.918674 424.070685 -L 93.939876 428.184053 -L 94.750447 431.398908 -L 96.185261 433.841039 -L 97.403511 438.13015 -L 97.620757 440.146148 -L 96.628085 442.278812 -L 96.941826 449.331787 -L 96.191149 452.353649 -L 95.384269 454.073268 -L 96.260136 457.81039 -L 95.859661 459.92194 -L 96.21491 462.310481 -L 94.787083 464.187848 -L 92.5817 463.176726 -L 91.585486 463.440854 -L 88.460209 461.452549 -L 87.02588 461.374035 -L 84.361795 458.967626 -L 85.469066 461.042093 -L 90.338351 464.833726 -L 91.735705 467.503495 -L 94.199463 469.388125 -L 95.313288 471.265156 -L 96.546836 475.890902 -L 94.925575 477.484018 -L 91.596024 477.673635 -L 89.284001 476.964028 -L 91.339455 479.267057 -L 93.102031 481.988069 -L 94.88536 483.898062 -L 94.855977 485.007723 -L 93.516937 485.208923 -L 90.850419 483.591263 -L 91.00993 484.433611 -L 94.235548 488.076436 -L 96.230774 489.35299 -L 96.166366 488.383734 -L 98.232766 490.325733 -L 97.688315 491.160815 -L 98.36934 493.11973 -L 101.336886 496.573202 -L 102.710664 498.341479 -L 101.356986 498.083814 -L 101.952936 499.603473 -L 104.068923 502.043902 -L 107.855361 504.741356 -L 109.859947 505.664402 -L 112.424944 508.477299 -L 112.804239 509.963292 -L 116.114667 513.472105 -L 116.437223 514.452768 -L 117.661029 515.780836 -L 121.826197 518.991414 -L 124.715294 520.866951 -L 124.05072 520.613672 -L 123.043261 520.36901 -L 123.450433 520.970181 -L 123.889556 521.79813 -L 127.292199 524.44178 -L 126.976933 524.404021 -L 124.687071 523.197564 -L 121.005222 520.89144 -L 117.778824 518.883244 -L 115.106466 516.902546 -L 113.112657 515.196126 -L 110.202462 513.009729 -L 103.786673 507.575879 -L 100.658868 504.564729 -L 98.857382 502.310298 -L 96.464019 500.895321 -L 94.21296 498.183829 -L 88.853536 492.534973 -L 91.155358 494.101157 -L 84.870506 486.908426 -L 83.283232 485.737797 -L 87.040655 490.126897 -L 85.938777 489.432057 -L 81.58264 484.365985 -L 76.090492 477.607172 -L 74.498032 475.172751 -L 71.23239 471.345948 -L 67.816838 466.941729 -L 68.205188 466.955323 -L 64.343492 460.769785 -L 60.857084 454.544088 -L 57.528419 448.557243 -L 53.465972 442.199625 -L 51.879629 438.805978 -L 48.818525 433.466886 -L 46.902967 428.376321 -L 43.046311 419.912204 -L 39.425474 410.708294 -L 35.867297 400.613089 -L 33.030834 392.996865 -L 30.521738 386.31723 -L 28.106521 383.215096 -L 27.383762 381.238822 -L 23.113219 375.67338 -L 19.330281 369.711542 -L 17.625504 366.430591 -L 16.152945 362.271333 -L 16.009394 360.944039 -L 13.62284 354.215943 -L 10.804637 344.776902 -L 8.276745 334.568426 -L 7.577428 332.140118 -L 6.815331 328.357457 -L 5.765483 324.786 -L 4.980224 322.467608 -L 5.002632 320.387214 -L 4.199718 315.427832 -L 4.170789 312.069263 -L 4.613551 309.260061 -L 4.880197 305.755512 -L 4.597314 303.478625 -L 4.368334 305.636025 -L 3.787028 303.185957 -L 3.897446 301.863946 -L 3.643751 297.2492 -L 3.913661 296.64633 -L 4.034376 293.606905 -L 4.359996 290.544646 -L 4.319344 288.471195 -L 4.822315 287.633559 -L 5.510311 285.936077 -L 5.420426 284.323513 -L 5.795586 284.107227 -L 5.838598 281.580538 -L 6.148755 279.866603 -L 6.674337 279.7436 -L 7.286025 276.778434 -L 7.86844 274.316772 -L 7.543263 272.968925 -L 7.966826 270.147356 -L 8.121791 265.481048 -L 8.483427 264.262401 -L 8.764313 259.976386 -L 8.650136 257.150123 -L 8.487953 255.572602 -L 8.631856 252.796979 -L 9.068248 251.574396 -L 8.861728 251.126944 -L 8.91933 249.407461 -L 8.652829 247.816678 -L 8.202013 247.946001 -L 7.759206 249.586217 -L 7.226254 250.660648 -L 7.018025 250.740197 -L 6.789783 251.732587 -L 6.826666 254.634853 -L 6.511238 255.153809 -L 6.2966 255.8293 -L 5.887892 255.883375 -L 6.140604 252.83827 -L 5.926965 253.62742 -L 5.719086 253.193364 -L 5.847784 251.108527 -L 5.608126 250.601777 -L 5.517778 249.909459 -L 5.241917 249.732845 -L 5.068735 250.796704 -L 5.105111 249.996732 -L 4.947757 248.662526 -L 4.99143 247.533593 -L 5.186503 246.722765 -L 5.338033 245.612228 -L 5.379393 244.307836 -L 5.339388 243.165963 -L 5.281402 242.385041 -L 5.499486 240.910758 -L 5.538115 239.915867 -L 5.314892 241.378967 -L 5.027789 242.47418 -L 5.16749 241.00516 -L 5.127684 240.378891 -L 5.253358 239.324868 -L 5.527932 237.825906 -L 5.867054 236.330401 -L 5.89775 235.51461 -L 6.164694 234.66216 -L 6.350341 232.964915 -L 6.608525 230.783805 -L 6.896144 229.036009 -L 7.122241 227.316992 -L 7.50948 224.894933 -L 7.706831 224.186396 -L 7.572499 224.99216 -L 7.669031 224.679846 -L 7.987811 223.079451 -L 8.02754 222.525665 -L 7.735784 223.626607 -L 7.66332 223.307593 -L 7.718398 222.803609 -L 9.340942 216.748189 + <path clip-path="url(#p9f79fa544f)" d="M 10.963487 210.69277 +L 10.912767 210.977623 +L 10.875742 211.209563 +L 11.152642 210.454176 +L 11.226543 210.505237 +L 11.13371 210.937808 +L 11.235107 210.763836 +L 11.224053 211.394577 +L 11.389577 211.462787 +L 11.641916 211.064455 +L 11.835202 210.573582 +L 11.875301 210.957323 +L 11.87132 211.385892 +L 12.007067 211.381758 +L 12.208577 211.047251 +L 12.219987 211.907823 +L 12.263912 212.081695 +L 12.159347 213.112141 +L 12.007414 214.318088 +L 12.001568 215.212494 +L 11.792682 216.655832 +L 11.619556 217.085464 +L 11.306081 218.147627 +L 10.907413 219.969583 +L 10.360926 221.530696 +L 9.823023 223.447232 +L 9.297138 225.585274 +L 9.037251 226.86853 +L 8.55909 229.08377 +L 8.369744 229.50019 +L 7.870834 231.559234 +L 7.639147 232.885529 +L 7.274676 234.055859 +L 7.036147 235.405294 +L 6.961908 236.279318 +L 6.600544 239.066645 +L 6.471221 241.219072 +L 6.401928 243.530364 +L 6.346986 245.460064 +L 6.166323 246.500457 +L 6.406776 246.894659 +L 6.537297 246.531619 +L 6.547999 247.867688 +L 6.977072 247.720092 +L 7.512821 246.649808 +L 8.184624 245.909729 +L 8.729069 244.574113 +L 9.188963 245.099485 +L 9.07385 245.570054 +L 9.584064 245.975996 +L 9.894549 247.02064 +L 10.000626 248.650277 +L 10.19602 250.181197 +L 10.745986 250.557205 +L 12.151086 247.408183 +L 12.733744 247.053314 +L 13.014872 245.418208 +L 13.970163 241.314522 +L 15.118201 239.261132 +L 15.96189 239.428643 +L 16.264237 238.433158 +L 17.261448 239.167465 +L 18.876494 236.999502 +L 19.674862 236.056993 +L 20.896448 233.883074 +L 21.38342 234.328112 +L 21.531809 235.729353 +L 20.892806 237.310318 +L 20.620797 238.463862 +L 19.599643 238.801121 +L 19.681782 241.17548 +L 19.19921 243.757257 +L 18.030352 246.452983 +L 17.990237 250.544947 +L 18.718782 250.398917 +L 19.645912 246.904237 +L 19.442835 245.034235 +L 20.026075 241.267113 +L 22.421628 239.764824 +L 22.639544 237.382403 +L 23.535897 235.96927 +L 23.468427 239.579077 +L 24.678249 239.940812 +L 25.344183 242.958327 +L 25.128632 244.612513 +L 26.801549 245.013948 +L 28.971627 244.927532 +L 29.753686 247.372368 +L 31.237322 248.285489 +L 32.679209 246.953913 +L 32.911809 245.710066 +L 35.673218 245.88953 +L 38.419561 246.277713 +L 36.228292 247.419096 +L 36.634844 249.892877 +L 38.437342 250.571185 +L 39.884571 253.303621 +L 39.72266 257.352133 +L 41.016321 257.435886 +L 41.840187 258.758198 +L 43.275132 260.837998 +L 44.498198 264.321211 +L 44.299013 266.904432 +L 45.275992 267.162828 +L 46.464485 269.799829 +L 47.371258 271.687018 +L 50.587646 273.124812 +L 50.965623 272.261266 +L 53.29 272.189932 +L 56.299274 273.917272 +L 57.25841 274.587875 +L 59.370824 276.030234 +L 62.317038 280.625963 +L 62.707824 282.736341 +L 63.784895 282.616258 +L 64.413981 285.47342 +L 65.864185 294.434601 +L 67.562309 295.441792 +L 67.584825 298.910584 +L 65.135773 302.781602 +L 66.12804 304.396451 +L 71.8996 305.771605 +L 72.068077 310.798636 +L 74.573118 307.764296 +L 78.888033 309.966673 +L 84.761213 313.540957 +L 86.572013 316.618144 +L 86.066722 319.330151 +L 90.219731 318.14638 +L 97.512995 321.358699 +L 103.190402 321.590782 +L 109.08662 326.121165 +L 114.397163 332.029077 +L 117.55972 333.660892 +L 121.032611 334.079062 +L 122.595651 335.732982 +L 124.374932 342.109231 +L 125.267693 345.130331 +L 124.319633 353.141137 +L 122.570176 356.202158 +L 117.700346 362.608079 +L 115.901105 367.908057 +L 113.627772 371.883941 +L 112.684207 371.911509 +L 112.158391 375.361986 +L 113.930642 384.291693 +L 114.26724 391.472255 +L 114.504003 394.524713 +L 113.727248 396.277883 +L 114.470165 402.417045 +L 112.289979 408.128999 +L 112.928835 412.77324 +L 110.64276 414.51204 +L 110.609118 417.143441 +L 106.900812 416.836534 +L 102.185409 418.118937 +L 100.523404 419.905405 +L 97.371636 420.884327 +L 94.918674 424.070685 +L 93.939876 428.184053 +L 94.750447 431.398908 +L 96.185261 433.841039 +L 97.403511 438.13015 +L 97.620757 440.146148 +L 96.628085 442.278812 +L 96.941826 449.331787 +L 96.191149 452.353649 +L 95.384269 454.073268 +L 96.260136 457.81039 +L 95.859661 459.92194 +L 96.21491 462.310481 +L 94.787083 464.187848 +L 92.5817 463.176726 +L 91.585486 463.440854 +L 88.460209 461.452549 +L 87.02588 461.374035 +L 84.361795 458.967626 +L 85.469066 461.042093 +L 90.338351 464.833726 +L 91.735705 467.503495 +L 94.199463 469.388125 +L 95.313288 471.265156 +L 96.546836 475.890902 +L 94.925575 477.484018 +L 91.596024 477.673635 +L 89.284001 476.964028 +L 91.339455 479.267057 +L 93.102031 481.988069 +L 94.88536 483.898062 +L 94.855977 485.007723 +L 93.516937 485.208923 +L 90.850419 483.591263 +L 91.00993 484.433611 +L 94.235548 488.076436 +L 96.230774 489.35299 +L 96.166366 488.383734 +L 98.232766 490.325733 +L 97.688315 491.160815 +L 98.36934 493.11973 +L 101.336886 496.573202 +L 102.710664 498.341479 +L 101.356986 498.083814 +L 101.952936 499.603473 +L 104.068923 502.043902 +L 107.855361 504.741356 +L 109.859947 505.664402 +L 112.424944 508.477299 +L 112.804239 509.963292 +L 116.114667 513.472105 +L 116.437223 514.452768 +L 117.661029 515.780836 +L 121.826197 518.991414 +L 124.715294 520.866951 +L 124.05072 520.613672 +L 123.043261 520.36901 +L 123.450433 520.970181 +L 123.889556 521.79813 +L 127.292199 524.44178 +L 126.976933 524.404021 +L 124.687071 523.197564 +L 121.005222 520.89144 +L 117.778824 518.883244 +L 115.106466 516.902546 +L 113.112657 515.196126 +L 110.202462 513.009729 +L 103.786673 507.575879 +L 100.658868 504.564729 +L 98.857382 502.310298 +L 96.464019 500.895321 +L 94.21296 498.183829 +L 88.853536 492.534973 +L 91.155358 494.101157 +L 84.870506 486.908426 +L 83.283232 485.737797 +L 87.040655 490.126897 +L 85.938777 489.432057 +L 81.58264 484.365985 +L 76.090492 477.607172 +L 74.498032 475.172751 +L 71.23239 471.345948 +L 67.816838 466.941729 +L 68.205188 466.955323 +L 64.343492 460.769785 +L 60.857084 454.544088 +L 57.528419 448.557243 +L 53.465972 442.199625 +L 51.879629 438.805978 +L 48.818525 433.466886 +L 46.902967 428.376321 +L 43.046311 419.912204 +L 39.425474 410.708294 +L 35.867297 400.613089 +L 33.030834 392.996865 +L 30.521738 386.31723 +L 28.106521 383.215096 +L 27.383762 381.238822 +L 23.113219 375.67338 +L 19.330281 369.711542 +L 17.625504 366.430591 +L 16.152945 362.271333 +L 16.009394 360.944039 +L 13.62284 354.215943 +L 10.804637 344.776902 +L 8.276745 334.568426 +L 7.577428 332.140118 +L 6.815331 328.357457 +L 5.765483 324.786 +L 4.980224 322.467608 +L 5.002632 320.387214 +L 4.199718 315.427832 +L 4.170789 312.069263 +L 4.613551 309.260061 +L 4.880197 305.755512 +L 4.597314 303.478625 +L 4.368334 305.636025 +L 3.787028 303.185957 +L 3.897446 301.863946 +L 3.643751 297.2492 +L 3.913661 296.64633 +L 4.034376 293.606905 +L 4.359996 290.544646 +L 4.319344 288.471195 +L 4.822315 287.633559 +L 5.510311 285.936077 +L 5.420426 284.323513 +L 5.795586 284.107227 +L 5.838598 281.580538 +L 6.148755 279.866603 +L 6.674337 279.7436 +L 7.286025 276.778434 +L 7.86844 274.316772 +L 7.543263 272.968925 +L 7.966826 270.147356 +L 8.121791 265.481048 +L 8.483427 264.262401 +L 8.764313 259.976386 +L 8.650136 257.150123 +L 8.487953 255.572602 +L 8.631856 252.796979 +L 9.068248 251.574396 +L 8.861728 251.126944 +L 8.91933 249.407461 +L 8.652829 247.816678 +L 8.202013 247.946001 +L 7.759206 249.586217 +L 7.226254 250.660648 +L 7.018025 250.740197 +L 6.789783 251.732587 +L 6.826666 254.634853 +L 6.511238 255.153809 +L 6.2966 255.8293 +L 5.887892 255.883375 +L 6.140604 252.83827 +L 5.926965 253.62742 +L 5.719086 253.193364 +L 5.847784 251.108527 +L 5.608126 250.601777 +L 5.517778 249.909459 +L 5.241917 249.732845 +L 5.068735 250.796704 +L 5.105111 249.996732 +L 4.947757 248.662526 +L 4.99143 247.533593 +L 5.186503 246.722765 +L 5.338033 245.612228 +L 5.379393 244.307836 +L 5.339388 243.165963 +L 5.281402 242.385041 +L 5.499486 240.910758 +L 5.538115 239.915867 +L 5.314892 241.378967 +L 5.027789 242.47418 +L 5.16749 241.00516 +L 5.127684 240.378891 +L 5.253358 239.324868 +L 5.527932 237.825906 +L 5.867054 236.330401 +L 5.89775 235.51461 +L 6.164694 234.66216 +L 6.350341 232.964915 +L 6.608525 230.783805 +L 6.896144 229.036009 +L 7.122241 227.316992 +L 7.50948 224.894933 +L 7.706831 224.186396 +L 7.572499 224.99216 +L 7.669031 224.679846 +L 7.987811 223.079451 +L 8.02754 222.525665 +L 7.735784 223.626607 +L 7.66332 223.307593 +L 7.718398 222.803609 +L 9.340942 216.748189 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 20.011979 183.590734 -L 20.112015 183.471038 -L 20.185272 183.994134 -L 20.471848 183.846334 -L 20.142933 184.934134 -L 19.264423 187.174399 -L 18.089479 189.745908 -L 16.996992 192.518654 -L 16.742065 193.416825 -L 15.99512 195.401609 -L 14.694315 199.003692 -L 15.058377 197.67674 -L 15.000463 197.668691 -L 14.76776 198.368464 -L 14.788895 198.469005 -L 14.359239 199.756991 -L 13.668635 201.752387 -L 13.446771 202.528439 -L 12.89654 204.196538 -L 12.764521 204.681119 -L 12.015016 207.056372 -L 11.639251 208.170784 -L 14.095723 199.003106 + <path clip-path="url(#p9f79fa544f)" d="M 20.011979 183.590734 +L 20.112015 183.471038 +L 20.185272 183.994134 +L 20.471848 183.846334 +L 20.142933 184.934134 +L 19.264423 187.174399 +L 18.089479 189.745908 +L 16.996992 192.518654 +L 16.742065 193.416825 +L 15.99512 195.401609 +L 14.694315 199.003692 +L 15.058377 197.67674 +L 15.000463 197.668691 +L 14.76776 198.368464 +L 14.788895 198.469005 +L 14.359239 199.756991 +L 13.668635 201.752387 +L 13.446771 202.528439 +L 12.89654 204.196538 +L 12.764521 204.681119 +L 12.015016 207.056372 +L 11.639251 208.170784 +L 14.095723 199.003106 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 131.688042 46.580163 -L 132.363747 46.393489 -L 130.539797 48.014977 -L 129.014959 49.304913 -L 127.122615 50.801221 -L 126.183203 51.887887 -L 124.677824 53.355952 -L 124.919245 53.932911 -L 124.914048 54.483049 -L 121.329098 56.988247 -L 117.414011 60.089983 -L 113.300102 63.730861 -L 110.358559 67.130123 -L 112.054048 66.299794 -L 116.375503 63.207883 -L 122.322257 58.246032 -L 124.060082 56.501144 -L 126.832947 55.515434 -L 130.358953 53.747116 -L 133.545718 51.879166 -L 136.230326 49.97391 -L 139.331143 47.470445 -L 141.812809 45.166179 -L 146.876324 42.66446 -L 150.50174 40.252534 -L 157.083809 36.331645 -L 158.432115 35.915824 -L 158.181679 36.909278 -L 158.378583 37.349067 -L 160.029453 36.872181 -L 159.20821 37.88097 -L 157.650573 39.563025 -L 156.258279 40.603456 -L 157.392271 41.151872 -L 153.916382 43.320375 -L 149.082221 46.760965 -L 149.312393 47.379207 -L 147.684075 49.156115 -L 151.363576 47.926887 -L 156.72045 45.13149 -L 159.258327 44.026964 -L 156.61755 46.57169 -L 153.255482 50.359496 -L 150.166139 54.01885 -L 147.317928 55.771775 -L 147.060673 57.732469 -L 146.289863 59.64989 -L 147.757375 60.78138 -L 147.646167 61.900471 -L 145.380458 64.627527 -L 146.208818 65.215252 -L 145.620357 66.500423 -L 142.032287 70.146383 -L 139.541683 71.216651 -L 137.156983 72.211209 -L 133.224711 73.006717 -L 128.435683 75.45509 -L 125.23898 75.588232 -L 122.814631 74.347109 -L 120.773649 73.943569 -L 119.168916 73.910536 -L 115.566559 76.103155 -L 112.428931 77.267964 -L 106.070319 81.429276 -L 101.548109 84.341731 -L 103.082413 84.00189 -L 109.560056 79.863086 -L 115.119246 77.567019 -L 117.480269 77.617633 -L 117.051611 79.52855 -L 113.420705 81.606961 -L 110.255356 85.48332 -L 108.247312 88.270394 -L 108.628078 90.432276 -L 111.777611 90.352618 -L 117.041523 86.566459 -L 114.853314 89.191316 -L 114.836778 90.676122 -L 110.643248 92.743176 -L 105.094885 94.33036 -L 102.242964 95.553693 -L 98.24863 97.926054 -L 97.344735 97.430903 -L 99.993654 94.269102 -L 105.242722 91.717352 -L 102.763963 91.389349 -L 100.800941 91.519217 -L 99.968052 92.769391 -L 96.926127 94.246441 -L 94.445822 95.224656 -L 92.144514 96.012482 -L 89.314872 98.287327 -L 88.630571 98.869766 -L 86.914406 100.872677 -L 85.638008 102.995441 -L 86.083657 103.218931 -L 87.104027 101.791408 -L 86.783801 102.731751 -L 85.781592 103.803111 -L 84.417326 104.217631 -L 83.886006 103.994047 -L 82.45096 104.43453 -L 81.787454 104.491045 -L 80.97367 104.498481 -L 79.149493 105.351999 -L 81.374588 105.088523 -L 81.125352 105.913902 -L 78.595986 106.633743 -L 77.90604 106.438289 -L 78.323348 105.972763 -L 77.132989 106.950613 -L 77.297448 107.223212 -L 74.834255 109.962074 -L 71.716924 112.763179 -L 72.427583 111.721312 -L 72.367275 111.443853 -L 72.816487 110.351073 -L 71.363949 112.545304 -L 71.055128 113.33282 -L 69.939478 114.844528 -L 68.448049 116.293699 -L 65.519282 119.324936 -L 65.548529 119.132937 -L 67.872409 116.493316 -L 68.533833 114.791976 -L 70.963562 111.453909 -L 69.459187 113.102112 -L 67.730855 115.703393 -L 67.607028 114.893615 -L 67.260018 116.375181 -L 64.475513 120.233059 -L 64.549229 120.612019 -L 63.642406 122.057918 -L 60.913015 126.220381 -L 58.197183 129.082576 -L 56.401226 129.957063 -L 54.200209 132.184059 -L 53.601701 132.276016 -L 52.186457 133.642615 -L 51.163204 135.010513 -L 48.597244 137.371933 -L 46.945093 139.199308 -L 45.084784 141.571655 -L 43.162741 144.58488 -L 41.531294 147.674325 -L 39.65674 151.552518 -L 38.277424 154.85088 -L 37.267444 156.773215 -L 34.973351 162.148159 -L 33.456687 165.180018 -L 32.578647 166.92566 -L 31.084267 169.607342 -L 30.594522 170.066399 -L 30.496969 169.320626 -L 31.323684 167.261874 -L 31.566949 166.069623 -L 33.130079 161.979674 -L 34.594868 158.370061 -L 35.417325 156.567798 -L 37.097115 153.700072 -L 38.264732 151.166088 -L 39.925889 147.20442 -L 40.128937 146.388534 -L 38.584693 147.986895 -L 38.642716 147.701387 -L 39.644284 145.484605 -L 40.083963 144.174438 -L 39.50592 144.292022 -L 39.649871 143.478555 -L 39.405869 143.487838 -L 39.03246 143.932161 -L 38.329998 145.227885 -L 37.319515 146.986841 -L 36.843009 147.930683 -L 36.51065 148.432144 -L 36.86094 147.645961 -L 36.394774 148.331301 -L 36.406857 148.274226 -L 38.584684 144 -L 55.003106 118.717847 -L 73.97429 95.290385 -L 95.290385 73.97429 -L 118.717847 55.003106 + <path clip-path="url(#p9f79fa544f)" d="M 131.688042 46.580163 +L 132.363747 46.393489 +L 130.539797 48.014977 +L 129.014959 49.304913 +L 127.122615 50.801221 +L 126.183203 51.887887 +L 124.677824 53.355952 +L 124.919245 53.932911 +L 124.914048 54.483049 +L 121.329098 56.988247 +L 117.414011 60.089983 +L 113.300102 63.730861 +L 110.358559 67.130123 +L 112.054048 66.299794 +L 116.375503 63.207883 +L 122.322257 58.246032 +L 124.060082 56.501144 +L 126.832947 55.515434 +L 130.358953 53.747116 +L 133.545718 51.879166 +L 136.230326 49.97391 +L 139.331143 47.470445 +L 141.812809 45.166179 +L 146.876324 42.66446 +L 150.50174 40.252534 +L 157.083809 36.331645 +L 158.432115 35.915824 +L 158.181679 36.909278 +L 158.378583 37.349067 +L 160.029453 36.872181 +L 159.20821 37.88097 +L 157.650573 39.563025 +L 156.258279 40.603456 +L 157.392271 41.151872 +L 153.916382 43.320375 +L 149.082221 46.760965 +L 149.312393 47.379207 +L 147.684075 49.156115 +L 151.363576 47.926887 +L 156.72045 45.13149 +L 159.258327 44.026964 +L 156.61755 46.57169 +L 153.255482 50.359496 +L 150.166139 54.01885 +L 147.317928 55.771775 +L 147.060673 57.732469 +L 146.289863 59.64989 +L 147.757375 60.78138 +L 147.646167 61.900471 +L 145.380458 64.627527 +L 146.208818 65.215252 +L 145.620357 66.500423 +L 142.032287 70.146383 +L 139.541683 71.216651 +L 137.156983 72.211209 +L 133.224711 73.006717 +L 128.435683 75.45509 +L 125.23898 75.588232 +L 122.814631 74.347109 +L 120.773649 73.943569 +L 119.168916 73.910536 +L 115.566559 76.103155 +L 112.428931 77.267964 +L 106.070319 81.429276 +L 101.548109 84.341731 +L 103.082413 84.00189 +L 109.560056 79.863086 +L 115.119246 77.567019 +L 117.480269 77.617633 +L 117.051611 79.52855 +L 113.420705 81.606961 +L 110.255356 85.48332 +L 108.247312 88.270394 +L 108.628078 90.432276 +L 111.777611 90.352618 +L 117.041523 86.566459 +L 114.853314 89.191316 +L 114.836778 90.676122 +L 110.643248 92.743176 +L 105.094885 94.33036 +L 102.242964 95.553693 +L 98.24863 97.926054 +L 97.344735 97.430903 +L 99.993654 94.269102 +L 105.242722 91.717352 +L 102.763963 91.389349 +L 100.800941 91.519217 +L 99.968052 92.769391 +L 96.926127 94.246441 +L 94.445822 95.224656 +L 92.144514 96.012482 +L 89.314872 98.287327 +L 88.630571 98.869766 +L 86.914406 100.872677 +L 85.638008 102.995441 +L 86.083657 103.218931 +L 87.104027 101.791408 +L 86.783801 102.731751 +L 85.781592 103.803111 +L 84.417326 104.217631 +L 83.886006 103.994047 +L 82.45096 104.43453 +L 81.787454 104.491045 +L 80.97367 104.498481 +L 79.149493 105.351999 +L 81.374588 105.088523 +L 81.125352 105.913902 +L 78.595986 106.633743 +L 77.90604 106.438289 +L 78.323348 105.972763 +L 77.132989 106.950613 +L 77.297448 107.223212 +L 74.834255 109.962074 +L 71.716924 112.763179 +L 72.427583 111.721312 +L 72.367275 111.443853 +L 72.816487 110.351073 +L 71.363949 112.545304 +L 71.055128 113.33282 +L 69.939478 114.844528 +L 68.448049 116.293699 +L 65.519282 119.324936 +L 65.548529 119.132937 +L 67.872409 116.493316 +L 68.533833 114.791976 +L 70.963562 111.453909 +L 69.459187 113.102112 +L 67.730855 115.703393 +L 67.607028 114.893615 +L 67.260018 116.375181 +L 64.475513 120.233059 +L 64.549229 120.612019 +L 63.642406 122.057918 +L 60.913015 126.220381 +L 58.197183 129.082576 +L 56.401226 129.957063 +L 54.200209 132.184059 +L 53.601701 132.276016 +L 52.186457 133.642615 +L 51.163204 135.010513 +L 48.597244 137.371933 +L 46.945093 139.199308 +L 45.084784 141.571655 +L 43.162741 144.58488 +L 41.531294 147.674325 +L 39.65674 151.552518 +L 38.277424 154.85088 +L 37.267444 156.773215 +L 34.973351 162.148159 +L 33.456687 165.180018 +L 32.578647 166.92566 +L 31.084267 169.607342 +L 30.594522 170.066399 +L 30.496969 169.320626 +L 31.323684 167.261874 +L 31.566949 166.069623 +L 33.130079 161.979674 +L 34.594868 158.370061 +L 35.417325 156.567798 +L 37.097115 153.700072 +L 38.264732 151.166088 +L 39.925889 147.20442 +L 40.128937 146.388534 +L 38.584693 147.986895 +L 38.642716 147.701387 +L 39.644284 145.484605 +L 40.083963 144.174438 +L 39.50592 144.292022 +L 39.649871 143.478555 +L 39.405869 143.487838 +L 39.03246 143.932161 +L 38.329998 145.227885 +L 37.319515 146.986841 +L 36.843009 147.930683 +L 36.51065 148.432144 +L 36.86094 147.645961 +L 36.394774 148.331301 +L 36.406857 148.274226 +L 38.584684 144 +L 55.003106 118.717847 +L 73.97429 95.290385 +L 95.290385 73.97429 +L 118.717847 55.003106 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 139.3042 41.634172 -L 138.964732 41.906858 -L 137.154942 43.029917 -L 138.229571 42.332044 + <path clip-path="url(#p9f79fa544f)" d="M 139.3042 41.634172 +L 138.964732 41.906858 +L 137.154942 43.029917 +L 138.229571 42.332044 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 204.703431 13.836173 -L 201.794455 14.87577 -L 205.766584 13.782125 -L 208.136728 13.492873 -L 203.583517 15.26587 -L 200.451926 16.282469 -L 202.359033 16.077398 -L 204.514416 15.568625 -L 202.925119 16.663667 -L 201.282071 17.632467 -L 199.336857 18.390685 -L 201.227884 18.251014 -L 198.878043 19.49909 -L 198.48788 20.535996 -L 197.258625 21.396376 -L 193.819736 23.306246 -L 190.547527 23.94815 -L 189.385124 25.604805 -L 189.687403 26.292106 -L 187.061483 28.441528 -L 188.215 28.824352 -L 185.065274 30.309533 -L 178.673645 32.640944 -L 179.319398 31.466616 -L 182.034113 29.086548 -L 180.49681 29.1478 -L 177.960094 30.380493 -L 176.341532 31.857846 -L 175.58217 33.140257 -L 174.854081 33.826844 -L 171.413925 36.286595 -L 168.165686 37.976031 -L 168.074442 37.082596 -L 169.235532 34.70896 -L 166.836916 37.030639 -L 165.307124 38.708589 -L 164.041434 39.611599 -L 163.244284 38.104825 -L 163.977474 36.284331 -L 165.18371 34.89523 -L 166.6325 34.296419 -L 167.863753 32.831181 -L 169.11435 31.465728 -L 167.853231 32.122998 -L 165.655475 31.950914 -L 166.800882 31.023424 -L 170.369042 29.521859 -L 171.33814 29.817955 -L 173.018091 29.900993 -L 174.40093 29.066382 -L 176.799831 28.057906 -L 181.845864 26.237546 -L 183.65178 25.293097 -L 184.966438 24.546593 -L 186.364972 23.356106 -L 187.006703 22.391973 -L 188.429801 22.01196 -L 190.850803 20.702649 -L 190.788153 20.46892 -L 192.095106 19.742233 -L 190.553903 20.191126 -L 189.431044 20.151951 -L 189.723404 19.210557 -L 190.85436 18.387984 -L 191.465405 17.892756 -L 193.108694 17.184676 -L 195.306248 16.586528 -L 195.294048 16.379156 -L 200.127317 14.746405 -L 204.422692 13.512319 -L 206.416883 13.051725 -L 207.881233 12.989326 + <path clip-path="url(#p9f79fa544f)" d="M 204.703431 13.836173 +L 201.794455 14.87577 +L 205.766584 13.782125 +L 208.136728 13.492873 +L 203.583517 15.26587 +L 200.451926 16.282469 +L 202.359033 16.077398 +L 204.514416 15.568625 +L 202.925119 16.663667 +L 201.282071 17.632467 +L 199.336857 18.390685 +L 201.227884 18.251014 +L 198.878043 19.49909 +L 198.48788 20.535996 +L 197.258625 21.396376 +L 193.819736 23.306246 +L 190.547527 23.94815 +L 189.385124 25.604805 +L 189.687403 26.292106 +L 187.061483 28.441528 +L 188.215 28.824352 +L 185.065274 30.309533 +L 178.673645 32.640944 +L 179.319398 31.466616 +L 182.034113 29.086548 +L 180.49681 29.1478 +L 177.960094 30.380493 +L 176.341532 31.857846 +L 175.58217 33.140257 +L 174.854081 33.826844 +L 171.413925 36.286595 +L 168.165686 37.976031 +L 168.074442 37.082596 +L 169.235532 34.70896 +L 166.836916 37.030639 +L 165.307124 38.708589 +L 164.041434 39.611599 +L 163.244284 38.104825 +L 163.977474 36.284331 +L 165.18371 34.89523 +L 166.6325 34.296419 +L 167.863753 32.831181 +L 169.11435 31.465728 +L 167.853231 32.122998 +L 165.655475 31.950914 +L 166.800882 31.023424 +L 170.369042 29.521859 +L 171.33814 29.817955 +L 173.018091 29.900993 +L 174.40093 29.066382 +L 176.799831 28.057906 +L 181.845864 26.237546 +L 183.65178 25.293097 +L 184.966438 24.546593 +L 186.364972 23.356106 +L 187.006703 22.391973 +L 188.429801 22.01196 +L 190.850803 20.702649 +L 190.788153 20.46892 +L 192.095106 19.742233 +L 190.553903 20.191126 +L 189.431044 20.151951 +L 189.723404 19.210557 +L 190.85436 18.387984 +L 191.465405 17.892756 +L 193.108694 17.184676 +L 195.306248 16.586528 +L 195.294048 16.379156 +L 200.127317 14.746405 +L 204.422692 13.512319 +L 206.416883 13.051725 +L 207.881233 12.989326 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 368.231599 21.631788 -L 368.049848 21.868775 -L 364.453784 22.012212 -L 362.49841 21.208995 -L 359.713722 20.913052 -L 357.551944 19.941728 -L 357.715631 19.448466 -L 355.611386 18.492223 -L 354.032164 16.843286 -L 352.791737 16.732568 -L 351.222351 15.088152 -L 349.201602 14.411892 -L 348.6142 13.413049 -L 347.961585 12.18955 -L 348.951459 11.625728 -L 348.134248 10.97265 -L 348.40946 10.624988 -L 350.500704 11.07518 -L 351.624297 11.55481 -L 352.113723 12.584884 -L 352.444617 13.572973 -L 354.395759 15.33411 -L 357.265113 17.068932 -L 359.811532 18.88105 -L 363.263556 20.342575 + <path clip-path="url(#p9f79fa544f)" d="M 368.231599 21.631788 +L 368.049848 21.868775 +L 364.453784 22.012212 +L 362.49841 21.208995 +L 359.713722 20.913052 +L 357.551944 19.941728 +L 357.715631 19.448466 +L 355.611386 18.492223 +L 354.032164 16.843286 +L 352.791737 16.732568 +L 351.222351 15.088152 +L 349.201602 14.411892 +L 348.6142 13.413049 +L 347.961585 12.18955 +L 348.951459 11.625728 +L 348.134248 10.97265 +L 348.40946 10.624988 +L 350.500704 11.07518 +L 351.624297 11.55481 +L 352.113723 12.584884 +L 352.444617 13.572973 +L 354.395759 15.33411 +L 357.265113 17.068932 +L 359.811532 18.88105 +L 363.263556 20.342575 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 353.403272 7.773831 -L 352.252415 7.510761 -L 349.994888 6.97857 -L 347.448787 6.355432 -L 348.494931 6.512738 -L 350.060307 6.884019 -L 350.066061 6.879628 -L 351.734666 7.32673 + <path clip-path="url(#p9f79fa544f)" d="M 353.403272 7.773831 +L 352.252415 7.510761 +L 349.994888 6.97857 +L 347.448787 6.355432 +L 348.494931 6.512738 +L 350.060307 6.884019 +L 350.066061 6.879628 +L 351.734666 7.32673 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 555.547571 182.443322 -L 555.455913 182.630794 -L 555.598044 184.101504 -L 556.97257 187.597879 -L 557.867986 190.597554 -L 558.510731 194.366729 -L 559.906495 200.418702 -L 560.476592 203.74388 -L 560.841293 207.296874 -L 561.501891 209.510819 -L 561.511855 210.905971 -L 561.3442 213.010316 -L 561.045562 213.459748 -L 561.808577 217.280217 -L 563.593368 223.523175 -L 564.591164 227.555835 -L 565.253296 232.393714 -L 566.883265 240.766078 -L 566.464126 241.224605 -L 566.687763 245.190868 -L 567.239628 246.693686 -L 566.57609 248.486478 -L 566.731084 252.006117 -L 566.506119 253.604825 -L 564.781631 249.366033 -L 563.099928 242.622183 -L 561.717907 237.801208 -L 560.827066 235.615814 -L 559.166459 231.112409 -L 557.4447 225.087628 -L 556.472541 222.109448 -L 553.504889 215.801782 -L 550.211315 206.615136 -L 547.835805 200.607669 -L 545.869251 194.847338 -L 543.996151 190.498012 -L 542.903018 193.890013 -L 541.613134 193.595682 -L 537.411142 188.381242 -L 537.55341 186.503022 -L 536.361909 184.7771 -L 532.829219 181.259488 -L 531.081087 180.356493 -L 529.18191 177.135909 -L 526.304598 173.906845 -L 523.195169 175.436734 -L 519.959583 176.099948 -L 517.236342 177.241599 -L 512.468196 176.496001 -L 509.499088 175.804035 -L 506.547141 175.582811 -L 503.207033 170.092321 -L 501.692657 169.438972 -L 500.11337 170.504107 -L 498.428813 173.04744 -L 494.604118 171.911875 -L 490.478046 168.711135 -L 487.315734 167.706997 -L 483.700047 163.602392 -L 479.100999 157.824414 -L 477.903361 158.706126 -L 475.466447 157.4062 -L 475.136872 159.259123 -L 473.324217 159.207509 -L 474.752314 161.131441 -L 474.910715 162.185012 -L 477.22264 165.504384 -L 479.903347 169.317361 -L 481.741392 170.217159 -L 482.853583 171.775398 -L 485.555715 173.503131 -L 486.440523 175.385771 -L 486.717663 176.954394 -L 487.64886 178.472107 -L 488.947648 179.684943 -L 489.873964 181.169514 -L 490.706293 182.265083 -L 489.312991 178.901015 -L 489.194548 176.38935 -L 489.782794 175.808638 -L 491.17485 177.164831 -L 492.217427 179.873553 -L 492.576659 182.679717 -L 493.739154 184.405178 -L 494.166132 184.121926 -L 494.71426 185.393679 -L 496.660332 184.408789 -L 498.986431 184.267273 -L 500.675183 184.2102 -L 501.358511 180.83542 -L 502.181044 177.617543 -L 502.678223 174.565313 -L 502.779918 172.904508 -L 503.249146 173.262862 -L 503.775038 175.194751 -L 503.778244 176.07448 -L 505.533768 179.687818 -L 507.853917 182.71715 -L 509.869427 184.224468 -L 511.8931 184.587301 -L 513.633701 185.237514 -L 515.683585 187.769654 -L 516.878084 189.238121 -L 517.929085 189.709173 -L 518.294697 190.761768 -L 518.405793 193.705703 -L 518.471212 195.091327 -L 517.957106 196.757398 -L 518.088844 200.150372 -L 516.88624 200.058581 -L 516.71959 201.269407 -L 517.051563 203.75176 -L 518.306099 206.917681 -L 518.237461 207.542308 -L 517.056037 207.686622 -L 515.93989 209.705012 -L 516.326608 212.09154 -L 515.993869 213.19171 -L 514.31831 213.367049 -L 513.574293 214.722197 -L 514.087475 216.68112 -L 513.091248 218.196268 -L 511.438647 217.92532 -L 509.941212 219.793932 -L 508.679063 220.225042 -L 506.874335 221.776718 -L 506.794034 224.00989 -L 507.09246 225.685895 -L 504.541166 228.084557 -L 500.058064 230.896602 -L 497.869643 234.655954 -L 496.495702 235.068025 -L 495.464186 234.871121 -L 493.910014 237.102384 -L 491.952037 238.25214 -L 489.159419 238.766858 -L 488.344538 239.124875 -L 487.810372 240.497915 -L 486.965229 240.938682 -L 486.630889 242.244086 -L 484.889817 242.284158 -L 483.873224 243.052461 -L 481.385591 243.004478 -L 480.007827 240.188986 -L 479.670398 237.477355 -L 478.844803 236.068475 -L 477.515784 232.479304 -L 476.128144 230.539682 -L 476.797936 230.242548 -L 476.016714 228.028454 -L 476.26777 227.04792 -L 475.697095 224.923262 -L 474.822905 222.872449 -L 473.455672 221.486573 -L 472.780957 219.563336 -L 470.577606 217.961785 -L 467.797054 214.034734 -L 465.906732 210.167543 -L 462.660029 207.033704 -L 460.865698 206.360924 -L 457.382069 201.958448 -L 456.149757 198.666096 -L 455.591863 195.831836 -L 452.154475 190.722165 -L 449.970154 188.997561 -L 447.750802 188.155508 -L 445.829375 185.542494 -L 445.737691 184.470425 -L 444.058231 182.112118 -L 442.70766 181.140149 -L 440.30988 177.773558 -L 437.022891 174.184638 -L 434.242671 171.139968 -L 432.437382 171.273028 -L 432.257444 168.72522 -L 431.934333 167.118627 -L 431.820985 165.27609 -L 431.496643 164.627257 -L 431.048781 166.513823 -L 431.302337 170.008748 -L 430.999281 172.457656 -L 430.369026 173.311668 -L 428.715843 171.898823 -L 426.463681 169.948138 -L 421.93053 163.569749 -L 421.673063 164.000886 -L 424.598451 168.7168 -L 428.180477 173.198963 -L 432.965621 180.246264 -L 435.016896 182.699284 -L 436.907827 185.270722 -L 441.591227 190.274089 -L 441.053976 191.143386 -L 441.930215 194.214618 -L 447.318417 198.207338 -L 448.205316 199.14174 -L 450.498118 203.759524 -L 449.873291 204.684672 -L 451.502171 209.607597 -L 454.08528 215.298488 -L 455.761946 216.397845 -L 458.176203 218.055363 -L 461.438468 223.552777 -L 463.289912 227.977766 -L 465.756853 230.222927 -L 471.593579 234.49882 -L 474.088121 237.155525 -L 476.512907 239.853103 -L 477.90844 241.463822 -L 479.903637 242.81099 -L 480.981212 244.276667 -L 481.171067 246.3459 -L 479.286611 247.700721 -L 481.023187 248.935008 -L 482.315307 249.755933 -L 483.290808 251.754571 -L 485.166901 253.704497 -L 486.923264 253.571674 -L 490.04462 252.010535 -L 493.6921 251.083707 -L 496.431802 249.260993 -L 498.028394 248.776128 -L 499.068211 247.755739 -L 500.895073 247.397244 -L 501.910976 247.195315 -L 503.27198 246.308105 -L 504.871949 245.633874 -L 506.080242 243.780594 -L 507.253196 243.640546 -L 507.54572 245.003189 -L 507.711778 247.921079 -L 508.112122 250.531469 -L 507.710864 252.398823 -L 507.537758 257.878667 -L 506.65861 263.615914 -L 505.303033 270.208444 -L 503.078603 277.832677 -L 500.576463 283.724504 -L 496.874633 290.944564 -L 493.512757 295.319759 -L 488.276602 300.760224 -L 484.914739 304.853165 -L 480.842393 311.241749 -L 479.943669 313.945162 -L 479.0741 315.194296 -L 476.385311 317.390417 -L 475.390874 319.543408 -L 473.948233 320.015444 -L 473.287654 323.567054 -L 471.973514 325.665226 -L 471.070959 329.027824 -L 469.429042 330.779001 -L 467.272849 337.010645 -L 467.311109 339.794842 -L 469.657814 341.417266 -L 469.672504 342.690379 -L 468.362845 345.738855 -L 468.452134 347.216471 -L 467.981748 349.578322 -L 469.017846 352.551802 -L 470.066024 357.250599 -L 471.328553 358.213367 -L 471.670321 360.347814 -L 470.881048 365.187406 -L 470.737504 369.385453 -L 469.6954 376.865793 -L 469.943991 379.151699 -L 468.238056 382.627747 -L 466.193436 386.017059 -L 463.27079 389.108195 -L 459.503462 391.132875 -L 454.792604 393.692551 -L 449.467532 398.96541 -L 447.813462 399.904861 -L 444.434853 403.349461 -L 442.612995 404.509185 -L 441.581654 407.792679 -L 442.630467 411.127638 -L 442.758523 413.735985 -L 442.479781 415.082244 -L 443.20501 414.815428 -L 441.995668 419.222624 -L 440.850779 421.330976 -L 441.534042 422.037076 -L 440.484812 423.916471 -L 438.496509 425.58355 -L 434.993118 427.257744 -L 429.795588 429.892552 -L 427.689327 431.598564 -L 427.515952 433.409111 -L 428.405052 433.646498 -L 427.43984 435.937447 -L 425.598726 439.112403 -L 424.106305 442.671109 -L 422.561121 444.63034 -L 419.363628 446.888867 -L 418.447179 447.532536 -L 416.208704 449.731984 -L 414.503969 451.913144 -L 411.459045 454.96468 -L 405.90997 459.363584 -L 402.507337 461.898792 -L 399.111132 463.8435 -L 394.715359 465.554597 -L 392.736778 465.842282 -L 391.913333 466.956865 -L 389.80135 466.470407 -L 387.703547 467.295151 -L 383.79754 466.698321 -L 381.339737 467.265009 -L 379.803801 467.117661 -L 375.406084 468.8059 -L 371.938439 469.535553 -L 369.166748 471.082631 -L 367.377888 471.231142 -L 366.065519 469.896825 -L 364.758 469.865313 -L 363.465553 468.170765 -L 363.155887 468.719103 -L 362.867127 467.68094 -L 363.399785 465.369118 -L 362.666115 462.741838 -L 364.118793 461.976609 -L 364.653069 458.89167 -L 362.722538 455.160212 -L 361.280604 451.733235 -L 361.27592 451.722564 -L 359.176583 446.389987 -L 356.505387 443.285228 -L 355.271933 440.222634 -L 354.884662 436.070663 -L 354.212472 432.961734 -L 353.54173 426.257918 -L 354.031862 420.956699 -L 353.713755 418.532082 -L 352.141914 416.720654 -L 350.146756 413.043171 -L 348.206305 407.634214 -L 347.42721 404.77704 -L 343.967162 400.347437 -L 343.921882 396.792363 -L 343.665306 393.867949 -L 344.571077 389.734213 -L 346.459385 385.386625 -L 346.821542 383.349324 -L 348.601344 379.03369 -L 349.843048 377.05501 -L 352.750326 373.866828 -L 354.39338 371.697565 -L 355.073666 368.117907 -L 354.952542 365.383804 -L 353.592156 363.682042 -L 352.431888 360.762444 -L 351.356942 357.864713 -L 351.652964 356.84613 -L 353.209339 354.879552 -L 351.893158 350.17752 -L 350.991766 346.908471 -L 348.629437 343.839315 -L 349.115887 342.872353 -L 348.465198 341.354206 -L 347.231957 337.660068 -L 343.282186 332.480513 -L 338.271672 327.544459 -L 335.031543 323.481282 -L 332.042616 318.36568 -L 332.20568 316.701971 -L 333.293008 315.087792 -L 334.498801 311.426125 -L 335.491704 307.695885 -L 334.561206 306.952961 -L 336.235816 301.292916 -L 336.927008 297.316586 -L 334.960635 294.024 -L 332.692035 293.203462 -L 331.660234 290.964728 -L 330.383502 290.260593 -L 330.42348 288.870807 -L 325.292175 290.743087 -L 323.403539 290.49963 -L 321.500154 291.645612 -L 317.51426 291.574431 -L 314.819975 288.452886 -L 313.148695 284.828052 -L 309.592856 281.542196 -L 305.847818 281.627 -L 301.444558 281.644939 -L 297.320619 282.245408 -L 293.299989 283.326754 -L 285.459473 286.272363 -L 282.674371 287.993237 -L 278.159419 289.440436 -L 273.70407 287.996551 -L 271.428613 288.037103 -L 267.948088 287.034701 -L 264.747378 287.071875 -L 258.834539 287.902066 -L 255.364234 289.320217 -L 250.421768 291.110728 -L 249.463421 290.967024 -L 248.161972 290.995578 -L 243.082726 288.533589 -L 238.65372 284.64282 -L 234.513251 281.827371 -L 231.28316 278.529422 -L 229.974771 278.13311 -L 226.514971 276.044897 -L 224.06029 273.305205 -L 223.268867 271.455554 -L 222.801767 267.73616 -L 220.795767 264.726397 -L 219.005049 262.726054 -L 217.803416 262.051253 -L 216.651024 261.029598 -L 216.20997 258.807455 -L 215.564128 257.689757 -L 214.218172 256.838877 -L 211.789473 254.690783 -L 209.812834 254.310819 -L 208.807864 252.877424 -L 208.87947 252.119336 -L 207.50547 251.02448 -L 207.271422 249.950538 -L 206.746887 246.115471 -L 207.494167 243.927066 -L 205.844862 240.015033 -L 203.662654 238.18799 -L 205.775022 237.304529 -L 208.293228 233.895476 -L 209.596077 231.391604 -L 209.41271 228.729064 -L 210.904407 226.33891 -L 211.875943 221.740978 -L 211.812112 216.905956 -L 211.495192 214.473587 -L 212.187465 212.065246 -L 211.252934 209.733863 -L 209.104289 207.58469 -L 209.522222 205.553853 -L 209.984254 203.331231 -L 211.846214 202.067742 -L 213.587002 199.610031 -L 213.485567 197.985132 -L 215.400769 194.662318 -L 218.212664 191.706656 -L 219.787513 190.978963 -L 221.271834 188.251821 -L 221.66917 185.746972 -L 223.5913 182.884496 -L 226.697581 181.245442 -L 230.000136 176.553693 -L 232.466497 174.755831 -L 236.552408 174.32176 -L 240.287296 171.23259 -L 242.573716 170.045347 -L 246.528261 166.284371 -L 245.997162 160.631436 -L 247.979451 156.791046 -L 248.771818 154.448584 -L 251.741365 151.48634 -L 256.08505 149.521041 -L 259.301317 147.736945 -L 262.38539 143.242242 -L 263.855074 140.59362 -L 266.84259 140.639546 -L 269.193171 142.495721 -L 273.095458 142.219964 -L 277.303836 143.198522 -L 279.085812 143.250884 -L 283.067687 140.89322 -L 287.481265 140.147453 -L 290.041302 138.370502 -L 293.91858 137.059488 -L 300.721849 136.273508 -L 307.343586 135.886256 -L 309.405817 136.505449 -L 313.051312 134.790614 -L 317.301697 134.71319 -L 319.01224 135.686572 -L 321.707878 135.393686 -L 325.826673 133.617953 -L 328.647265 134.085592 -L 328.802024 136.242733 -L 331.948492 134.619513 -L 332.340356 135.432355 -L 330.63693 137.559684 -L 330.862324 139.547444 -L 332.384975 140.595649 -L 332.328527 144.358509 -L 329.910939 146.597554 -L 330.96232 148.976934 -L 333.095025 149.018059 -L 334.381614 151.097894 -L 336.034461 151.761665 -L 341.074676 153.197564 -L 342.751656 152.781374 -L 346.289957 153.450192 -L 352.038893 155.31665 -L 354.594723 159.249643 -L 358.439926 160.028566 -L 364.561297 161.759327 -L 369.338499 163.880246 -L 371.115046 162.644604 -L 372.661579 160.50694 -L 371.038222 157.097434 -L 371.85281 154.885902 -L 374.271603 152.7097 -L 376.850701 152.013478 -L 382.355975 152.732643 -L 384.159139 154.66921 -L 385.616681 154.633096 -L 387.040955 155.345456 -L 391.028087 155.715636 -L 392.342051 157.152379 -L 397.451039 156.862608 -L 401.443333 157.882771 -L 405.575182 159.033158 -L 407.51954 159.64938 -L 410.010108 158.086295 -L 411.169069 156.726964 -L 414.3138 156.191633 -L 417.089852 156.617105 -L 418.765818 158.783611 -L 419.161653 157.274363 -L 422.397159 158.169924 -L 425.282644 158.262352 -L 426.672035 157.031459 -L 427.214994 155.499786 -L 426.889098 155.261921 -L 427.114816 153.129003 -L 426.657615 149.751811 -L 426.754216 148.608446 -L 426.833173 148.557863 -L 426.745571 144.92023 -L 427.258695 141.757913 -L 427.263216 141.603572 -L 425.655931 138.32981 -L 425.756698 136.519031 -L 423.758863 134.664378 -L 424.33905 132.994179 -L 422.494396 133.480331 -L 419.338894 132.676688 -L 418.017399 135.25297 -L 413.051318 136.026999 -L 409.425482 133.896374 -L 405.653697 133.949784 -L 405.479769 135.754388 -L 403.230161 136.383489 -L 399.039883 134.282752 -L 395.186153 134.540413 -L 391.6911 130.432225 -L 388.33347 128.221562 -L 388.951989 124.913103 -L 386.065792 123.037185 -L 388.49566 118.957536 -L 393.633833 118.553691 -L 393.817948 115.41661 -L 400.272175 115.632596 -L 402.99137 112.829078 -L 406.135846 111.498745 -L 411.195962 111.111146 -L 417.879397 113.576691 -L 422.96112 114.834548 -L 426.07805 113.975694 -L 428.768533 114.149751 -L 431.001573 111.816393 -L 430.469646 110.094206 -L 428.243664 107.471732 -L 425.81055 106.15772 -L 424.008915 105.826109 -L 422.303266 104.714151 -L 416.863835 101.720004 -L 412.8238 100.501165 -L 409.115168 98.436067 -L 410.824388 97.698716 -L 411.271637 94.417017 -L 408.857067 93.055937 -L 411.898902 91.282002 -L 411.319869 90.487355 -L 409.297174 91.243401 -L 407.31783 91.682768 -L 406.209765 92.993273 -L 403.721652 93.355032 -L 402.076246 94.887179 -L 403.532893 97.202542 -L 405.438919 98.023567 -L 408.222251 97.613233 -L 408.423392 98.995329 -L 405.62895 99.849886 -L 402.790605 102.297806 -L 400.732938 101.615242 -L 400.4716 99.786314 -L 396.656871 98.861046 -L 396.812832 98.107387 -L 398.975134 96.697308 -L 397.675427 95.888613 -L 392.622461 95.198512 -L 391.705355 93.824045 -L 389.204539 94.425405 -L 389.112341 96.537493 -L 388.084523 99.4435 -L 388.610195 100.419593 -L 387.481692 101.314368 -L 386.374081 101.002725 -L 387.525419 105.691166 -L 386.538791 107.383989 -L 386.495017 110.250136 -L 388.457264 112.462698 -L 389.456147 113.978874 -L 392.858649 115.136258 -L 392.647702 116.150758 -L 388.765672 116.557959 -L 387.793552 117.872958 -L 385.733652 120.188036 -L 383.982661 118.340031 -L 383.731981 117.501647 -L 381.604847 117.47453 -L 379.678357 117.165435 -L 375.832282 118.38398 -L 379.001401 120.57814 -L 377.433452 121.314738 -L 375.468519 121.393682 -L 372.945479 119.360049 -L 372.558082 120.279342 -L 374.09708 122.69766 -L 376.471947 124.566091 -L 375.392685 125.524391 -L 377.971368 127.365161 -L 380.121399 128.507278 -L 380.886855 130.875506 -L 377.192696 129.88965 -L 378.889683 131.996294 -L 376.681422 132.526543 -L 379.123991 136.224311 -L 376.662028 136.368031 -L 373.106413 134.622951 -L 370.818882 131.284569 -L 369.428171 128.51353 -L 367.481633 126.64323 -L 364.976319 124.339579 -L 364.415939 123.172088 -L 363.721027 122.903494 -L 363.406618 122.001613 -L 361.034571 120.68836 -L 360.205179 118.755925 -L 359.760782 115.985994 -L 359.899178 114.722242 -L 359.12897 114.108856 -L 358.303805 113.821624 -L 356.956778 112.548232 -L 355.221723 111.802775 -L 351.527317 110.429414 -L 349.12696 109.063885 -L 345.630717 107.978573 -L 342.006401 105.178201 -L 342.637938 104.872133 -L 340.673208 103.282937 -L 340.300912 101.986272 -L 337.953717 101.431704 -L 337.271763 103.1129 -L 335.968479 101.847354 -L 335.752363 100.51855 -L 335.860453 100.454277 -L 336.543479 100.091877 -L 333.702547 99.59516 -L 331.19216 101.00233 -L 331.76193 102.902785 -L 331.55007 104.010326 -L 333.08569 105.963296 -L 336.793434 107.864829 -L 339.261252 111.085764 -L 343.948655 114.20778 -L 346.765192 114.114469 -L 347.846609 114.978934 -L 347.017109 115.805561 -L 350.597965 117.181357 -L 353.552627 118.336087 -L 357.190657 120.371414 -L 357.759444 121.124557 -L 357.441651 122.613329 -L 354.943827 120.756337 -L 351.607272 120.16992 -L 350.657729 122.862232 -L 353.680696 124.321234 -L 353.72636 126.496908 -L 352.215442 126.784591 -L 350.952572 130.425793 -L 349.428056 130.789359 -L 349.18715 129.503617 -L 349.503088 127.242146 -L 350.134766 126.332541 -L 348.167787 123.96674 -L 346.597152 121.918477 -L 344.950763 121.442958 -L 343.49817 119.703735 -L 340.977989 119.01613 -L 339.067215 117.419522 -L 336.294744 117.213089 -L 333.096478 115.447181 -L 329.30896 112.898884 -L 326.471114 110.650652 -L 324.736616 106.780437 -L 322.893855 106.35397 -L 319.813781 105.111307 -L 318.248357 105.65749 -L 316.409525 107.494628 -L 314.94734 107.799606 -L 311.900912 110.05875 -L 304.625188 109.051105 -L 299.378679 110.362702 -L 299.06617 112.757175 -L 299.367886 115.082758 -L 295.906911 117.785539 -L 291.073888 118.654448 -L 290.749631 120.0256 -L 288.41007 122.297765 -L 286.916131 125.660626 -L 288.436495 128.042097 -L 286.157226 129.910785 -L 285.279914 132.652079 -L 282.260511 133.491119 -L 279.348582 136.759426 -L 274.234785 136.802799 -L 270.40446 136.698606 -L 267.796172 138.194089 -L 266.150957 139.806985 -L 264.194396 139.432402 -L 262.806424 137.966811 -L 261.862015 135.495569 -L 258.20061 134.795445 -L 256.496344 135.883028 -L 254.458489 135.257523 -L 252.358656 135.699278 -L 253.327629 132.383393 -L 253.250311 129.787368 -L 251.552851 129.376047 -L 250.819897 127.776455 -L 251.468038 125.057603 -L 253.179246 123.577984 -L 253.653762 121.918046 -L 254.744635 119.468621 -L 254.87489 117.745538 -L 254.313797 116.282705 -L 254.327365 114.923064 -L 254.889522 112.078012 -L 253.639068 110.336065 -L 259.123639 107.562712 -L 263.456557 108.320888 -L 268.321249 108.340877 -L 272.1372 109.041369 -L 275.173241 108.853313 -L 281.059952 109.007368 -L 282.997308 106.659967 -L 283.832997 98.992836 -L 280.381114 95.034689 -L 277.955185 93.145 -L 272.895133 91.696084 -L 272.776297 89.028529 -L 277.115933 88.264929 -L 282.631904 89.219168 -L 281.725279 85.137964 -L 284.76026 86.681233 -L 292.313478 83.894785 -L 293.190274 81.007739 -L 295.923005 80.295091 -L 298.402693 79.597857 -L 299.944967 78.63886 -L 302.190093 73.612441 -L 306.123213 72.179826 -L 308.604974 72.249212 -L 309.078837 71.539993 -L 311.514413 71.33147 -L 312.186204 72.056064 -L 313.887144 70.399024 -L 312.991914 69.178528 -L 312.514541 67.337673 -L 311.031603 65.570831 -L 310.346695 62.343442 -L 310.640931 61.49707 -L 311.233853 60.559051 -L 313.566159 60.33533 -L 314.311694 59.475439 -L 316.23311 58.581845 -L 316.542292 60.154572 -L 315.994537 61.172402 -L 316.530544 62.036828 -L 318.124774 62.48517 -L 317.749924 63.680112 -L 316.847061 63.351382 -L 315.365778 65.66422 -L 316.475376 67.216206 -L 316.799942 68.467319 -L 319.874468 69.180163 -L 320.118701 70.344633 -L 322.904058 69.676268 -L 324.277099 68.750374 -L 327.869739 69.976859 -L 329.537121 70.996163 -L 331.198459 69.985205 -L 335.102462 68.367219 -L 338.196705 67.178774 -L 341.162945 67.646025 -L 341.690732 68.430736 -L 344.370922 68.391054 -L 344.38408 66.948692 -L 347.636588 65.786799 -L 345.81493 63.144841 -L 344.771467 60.799993 -L 345.056847 58.833924 -L 346.866734 57.716872 -L 350.080298 59.914701 -L 352.057261 59.778293 -L 351.197976 57.430284 -L 350.44034 55.657708 -L 349.776352 56.066569 -L 347.635489 55.072849 -L 346.447036 53.396453 -L 348.920317 52.474139 -L 351.556515 51.936632 -L 354.351727 52.304687 -L 356.630941 52.113523 -L 358.012327 50.416167 -L 354.726232 49.185613 -L 350.884151 49.587997 -L 347.605408 50.788953 -L 344.223723 51.53411 -L 341.97267 50.030689 -L 339.216765 49.183576 -L 338.132563 46.439066 -L 335.613776 44.039628 -L 335.684382 42.464875 -L 336.511789 40.767287 -L 339.105882 37.851662 -L 339.986272 37.281398 -L 338.962098 36.253085 -L 335.452537 35.214756 -L 332.707365 36.018951 -L 331.998906 37.798437 -L 333.322051 39.323486 -L 331.318437 41.491358 -L 328.417358 43.876127 -L 328.659269 47.746278 -L 331.24443 49.669438 -L 334.262622 51.17888 -L 333.576289 54.512504 -L 331.294249 55.272916 -L 332.363605 60.364752 -L 331.947507 63.315914 -L 328.564672 63.090507 -L 327.834952 65.635277 -L 324.665056 65.849688 -L 322.920448 62.874709 -L 319.663313 59.404982 -L 316.491018 55.187036 -L 314.301676 53.412505 -L 310.052704 56.905536 -L 306.687436 57.651842 -L 302.819323 56.159934 -L 301.457597 53.002213 -L 299.776893 46.494784 -L 301.647027 44.730646 -L 307.143865 42.433029 -L 310.665591 39.701831 -L 313.189236 36.163039 -L 315.713603 31.507998 -L 317.781794 29.739656 -L 320.651531 26.888832 -L 323.466974 25.857499 -L 326.152004 25.875842 -L 326.892789 24.096492 -L 329.676076 24.073286 -L 331.842443 23.559853 -L 338.049289 24.802921 -L 336.819298 25.446097 -L 339.958208 26.667077 -L 340.628221 25.860025 -L 344.591763 26.986528 -L 349.235388 27.244401 -L 357.97332 29.304436 -L 360.524036 30.270514 -L 362.647629 31.761171 -L 362.534841 33.095355 -L 360.736022 33.901658 -L 351.414358 32.592381 -L 350.522169 32.957336 -L 355.140968 34.516222 -L 359.239512 38.112422 -L 362.3284 38.730979 -L 364.411036 39.293626 -L 363.315901 38.065612 -L 361.024372 37.036476 -L 361.15374 36.078586 -L 366.962011 37.317854 -L 367.638981 36.619864 -L 364.399472 34.934811 -L 365.05526 32.370744 -L 366.600495 32.393799 -L 369.07922 33.08735 -L 367.709849 31.441527 -L 364.61581 30.204102 -L 363.360065 28.834402 -L 360.367 27.584142 -L 365.09159 27.949857 -L 367.727057 29.090521 -L 366.464031 29.512544 -L 368.31541 30.745007 -L 370.534393 31.417648 -L 371.88452 30.734581 -L 370.00526 29.29355 -L 370.926398 28.002734 -L 371.738219 25.763545 -L 372.727733 25.767651 -L 373.956004 27.15488 -L 375.710289 27.210047 -L 375.165459 26.401805 -L 376.973931 26.093713 -L 376.762789 25.048373 -L 380.253931 26.118343 -L 378.581938 24.618006 -L 375.292523 23.614229 -L 374.391122 22.904605 -L 378.066827 23.092153 -L 380.456566 23.506708 -L 387.988684 25.242394 -L 386.155343 24.097864 -L 383.177353 23.215048 -L 382.23213 22.810401 -L 380.990805 22.793656 -L 379.156423 21.847887 -L 375.403557 20.478973 -L 373.987205 19.905096 -L 371.119235 18.084559 -L 367.416946 16.489462 -L 366.914552 16.082363 -L 369.370663 16.183283 -L 372.010138 17.063803 -L 375.248339 18.557165 -L 377.000905 19.027533 -L 380.326557 20.224257 -L 386.375602 22.804585 -L 389.707344 23.823632 -L 392.461187 25.216129 -L 397.550409 28.485046 -L 398.915633 28.6091 -L 397.573162 27.776239 -L 397.064775 27.054296 -L 395.005798 25.989173 -L 393.314483 24.886598 -L 390.468977 23.853441 -L 387.71263 22.507508 -L 386.811869 22.516378 -L 384.181106 21.505864 -L 379.993653 19.568477 -L 375.679897 18.325158 -L 373.36133 16.956888 -L 369.980856 15.836393 -L 370.067551 15.737953 -L 372.840889 16.561156 -L 377.146211 18.221438 -L 378.413263 18.388197 -L 374.901237 17.232793 -L 373.665154 16.36941 -L 374.043383 16.013487 -L 377.200848 16.686406 -L 373.051758 15.473213 -L 367.972743 13.862953 -L 367.376694 13.356694 -L 367.972507 13.128904 -L 367.566719 12.67962 -L 364.973477 12.049683 -L 361.884336 11.037414 -L 361.80971 10.872688 -L 359.427535 10.028671 -L 358.688574 9.580034 -L 357.33286 9.216739 -L 356.63027 8.832641 -L 357.610576 9.007681 -L 354.675728 8.17017 -L 354.512842 8.07114 -L 376.996894 14.095723 -L 405.140153 24.898908 -L 432 38.584684 -L 457.282153 55.003106 -L 480.709615 73.97429 -L 502.02571 95.290385 -L 520.996894 118.717847 -L 537.415316 144 -L 551.101092 170.859847 + <path clip-path="url(#p9f79fa544f)" d="M 555.547571 182.443322 +L 555.455913 182.630794 +L 555.598044 184.101504 +L 556.97257 187.597879 +L 557.867986 190.597554 +L 558.510731 194.366729 +L 559.906495 200.418702 +L 560.476592 203.74388 +L 560.841293 207.296874 +L 561.501891 209.510819 +L 561.511855 210.905971 +L 561.3442 213.010316 +L 561.045562 213.459748 +L 561.808577 217.280217 +L 563.593368 223.523175 +L 564.591164 227.555835 +L 565.253296 232.393714 +L 566.883265 240.766078 +L 566.464126 241.224605 +L 566.687763 245.190868 +L 567.239628 246.693686 +L 566.57609 248.486478 +L 566.731084 252.006117 +L 566.506119 253.604825 +L 564.781631 249.366033 +L 563.099928 242.622183 +L 561.717907 237.801208 +L 560.827066 235.615814 +L 559.166459 231.112409 +L 557.4447 225.087628 +L 556.472541 222.109448 +L 553.504889 215.801782 +L 550.211315 206.615136 +L 547.835805 200.607669 +L 545.869251 194.847338 +L 543.996151 190.498012 +L 542.903018 193.890013 +L 541.613134 193.595682 +L 537.411142 188.381242 +L 537.55341 186.503022 +L 536.361909 184.7771 +L 532.829219 181.259488 +L 531.081087 180.356493 +L 529.18191 177.135909 +L 526.304598 173.906845 +L 523.195169 175.436734 +L 519.959583 176.099948 +L 517.236342 177.241599 +L 512.468196 176.496001 +L 509.499088 175.804035 +L 506.547141 175.582811 +L 503.207033 170.092321 +L 501.692657 169.438972 +L 500.11337 170.504107 +L 498.428813 173.04744 +L 494.604118 171.911875 +L 490.478046 168.711135 +L 487.315734 167.706997 +L 483.700047 163.602392 +L 479.100999 157.824414 +L 477.903361 158.706126 +L 475.466447 157.4062 +L 475.136872 159.259123 +L 473.324217 159.207509 +L 474.752314 161.131441 +L 474.910715 162.185012 +L 477.22264 165.504384 +L 479.903347 169.317361 +L 481.741392 170.217159 +L 482.853583 171.775398 +L 485.555715 173.503131 +L 486.440523 175.385771 +L 486.717663 176.954394 +L 487.64886 178.472107 +L 488.947648 179.684943 +L 489.873964 181.169514 +L 490.706293 182.265083 +L 489.312991 178.901015 +L 489.194548 176.38935 +L 489.782794 175.808638 +L 491.17485 177.164831 +L 492.217427 179.873553 +L 492.576659 182.679717 +L 493.739154 184.405178 +L 494.166132 184.121926 +L 494.71426 185.393679 +L 496.660332 184.408789 +L 498.986431 184.267273 +L 500.675183 184.2102 +L 501.358511 180.83542 +L 502.181044 177.617543 +L 502.678223 174.565313 +L 502.779918 172.904508 +L 503.249146 173.262862 +L 503.775038 175.194751 +L 503.778244 176.07448 +L 505.533768 179.687818 +L 507.853917 182.71715 +L 509.869427 184.224468 +L 511.8931 184.587301 +L 513.633701 185.237514 +L 515.683585 187.769654 +L 516.878084 189.238121 +L 517.929085 189.709173 +L 518.294697 190.761768 +L 518.405793 193.705703 +L 518.471212 195.091327 +L 517.957106 196.757398 +L 518.088844 200.150372 +L 516.88624 200.058581 +L 516.71959 201.269407 +L 517.051563 203.75176 +L 518.306099 206.917681 +L 518.237461 207.542308 +L 517.056037 207.686622 +L 515.93989 209.705012 +L 516.326608 212.09154 +L 515.993869 213.19171 +L 514.31831 213.367049 +L 513.574293 214.722197 +L 514.087475 216.68112 +L 513.091248 218.196268 +L 511.438647 217.92532 +L 509.941212 219.793932 +L 508.679063 220.225042 +L 506.874335 221.776718 +L 506.794034 224.00989 +L 507.09246 225.685895 +L 504.541166 228.084557 +L 500.058064 230.896602 +L 497.869643 234.655954 +L 496.495702 235.068025 +L 495.464186 234.871121 +L 493.910014 237.102384 +L 491.952037 238.25214 +L 489.159419 238.766858 +L 488.344538 239.124875 +L 487.810372 240.497915 +L 486.965229 240.938682 +L 486.630889 242.244086 +L 484.889817 242.284158 +L 483.873224 243.052461 +L 481.385591 243.004478 +L 480.007827 240.188986 +L 479.670398 237.477355 +L 478.844803 236.068475 +L 477.515784 232.479304 +L 476.128144 230.539682 +L 476.797936 230.242548 +L 476.016714 228.028454 +L 476.26777 227.04792 +L 475.697095 224.923262 +L 474.822905 222.872449 +L 473.455672 221.486573 +L 472.780957 219.563336 +L 470.577606 217.961785 +L 467.797054 214.034734 +L 465.906732 210.167543 +L 462.660029 207.033704 +L 460.865698 206.360924 +L 457.382069 201.958448 +L 456.149757 198.666096 +L 455.591863 195.831836 +L 452.154475 190.722165 +L 449.970154 188.997561 +L 447.750802 188.155508 +L 445.829375 185.542494 +L 445.737691 184.470425 +L 444.058231 182.112118 +L 442.70766 181.140149 +L 440.30988 177.773558 +L 437.022891 174.184638 +L 434.242671 171.139968 +L 432.437382 171.273028 +L 432.257444 168.72522 +L 431.934333 167.118627 +L 431.820985 165.27609 +L 431.496643 164.627257 +L 431.048781 166.513823 +L 431.302337 170.008748 +L 430.999281 172.457656 +L 430.369026 173.311668 +L 428.715843 171.898823 +L 426.463681 169.948138 +L 421.93053 163.569749 +L 421.673063 164.000886 +L 424.598451 168.7168 +L 428.180477 173.198963 +L 432.965621 180.246264 +L 435.016896 182.699284 +L 436.907827 185.270722 +L 441.591227 190.274089 +L 441.053976 191.143386 +L 441.930215 194.214618 +L 447.318417 198.207338 +L 448.205316 199.14174 +L 450.498118 203.759524 +L 449.873291 204.684672 +L 451.502171 209.607597 +L 454.08528 215.298488 +L 455.761946 216.397845 +L 458.176203 218.055363 +L 461.438468 223.552777 +L 463.289912 227.977766 +L 465.756853 230.222927 +L 471.593579 234.49882 +L 474.088121 237.155525 +L 476.512907 239.853103 +L 477.90844 241.463822 +L 479.903637 242.81099 +L 480.981212 244.276667 +L 481.171067 246.3459 +L 479.286611 247.700721 +L 481.023187 248.935008 +L 482.315307 249.755933 +L 483.290808 251.754571 +L 485.166901 253.704497 +L 486.923264 253.571674 +L 490.04462 252.010535 +L 493.6921 251.083707 +L 496.431802 249.260993 +L 498.028394 248.776128 +L 499.068211 247.755739 +L 500.895073 247.397244 +L 501.910976 247.195315 +L 503.27198 246.308105 +L 504.871949 245.633874 +L 506.080242 243.780594 +L 507.253196 243.640546 +L 507.54572 245.003189 +L 507.711778 247.921079 +L 508.112122 250.531469 +L 507.710864 252.398823 +L 507.537758 257.878667 +L 506.65861 263.615914 +L 505.303033 270.208444 +L 503.078603 277.832677 +L 500.576463 283.724504 +L 496.874633 290.944564 +L 493.512757 295.319759 +L 488.276602 300.760224 +L 484.914739 304.853165 +L 480.842393 311.241749 +L 479.943669 313.945162 +L 479.0741 315.194296 +L 476.385311 317.390417 +L 475.390874 319.543408 +L 473.948233 320.015444 +L 473.287654 323.567054 +L 471.973514 325.665226 +L 471.070959 329.027824 +L 469.429042 330.779001 +L 467.272849 337.010645 +L 467.311109 339.794842 +L 469.657814 341.417266 +L 469.672504 342.690379 +L 468.362845 345.738855 +L 468.452134 347.216471 +L 467.981748 349.578322 +L 469.017846 352.551802 +L 470.066024 357.250599 +L 471.328553 358.213367 +L 471.670321 360.347814 +L 470.881048 365.187406 +L 470.737504 369.385453 +L 469.6954 376.865793 +L 469.943991 379.151699 +L 468.238056 382.627747 +L 466.193436 386.017059 +L 463.27079 389.108195 +L 459.503462 391.132875 +L 454.792604 393.692551 +L 449.467532 398.96541 +L 447.813462 399.904861 +L 444.434853 403.349461 +L 442.612995 404.509185 +L 441.581654 407.792679 +L 442.630467 411.127638 +L 442.758523 413.735985 +L 442.479781 415.082244 +L 443.20501 414.815428 +L 441.995668 419.222624 +L 440.850779 421.330976 +L 441.534042 422.037076 +L 440.484812 423.916471 +L 438.496509 425.58355 +L 434.993118 427.257744 +L 429.795588 429.892552 +L 427.689327 431.598564 +L 427.515952 433.409111 +L 428.405052 433.646498 +L 427.43984 435.937447 +L 425.598726 439.112403 +L 424.106305 442.671109 +L 422.561121 444.63034 +L 419.363628 446.888867 +L 418.447179 447.532536 +L 416.208704 449.731984 +L 414.503969 451.913144 +L 411.459045 454.96468 +L 405.90997 459.363584 +L 402.507337 461.898792 +L 399.111132 463.8435 +L 394.715359 465.554597 +L 392.736778 465.842282 +L 391.913333 466.956865 +L 389.80135 466.470407 +L 387.703547 467.295151 +L 383.79754 466.698321 +L 381.339737 467.265009 +L 379.803801 467.117661 +L 375.406084 468.8059 +L 371.938439 469.535553 +L 369.166748 471.082631 +L 367.377888 471.231142 +L 366.065519 469.896825 +L 364.758 469.865313 +L 363.465553 468.170765 +L 363.155887 468.719103 +L 362.867127 467.68094 +L 363.399785 465.369118 +L 362.666115 462.741838 +L 364.118793 461.976609 +L 364.653069 458.89167 +L 362.722538 455.160212 +L 361.280604 451.733235 +L 361.27592 451.722564 +L 359.176583 446.389987 +L 356.505387 443.285228 +L 355.271933 440.222634 +L 354.884662 436.070663 +L 354.212472 432.961734 +L 353.54173 426.257918 +L 354.031862 420.956699 +L 353.713755 418.532082 +L 352.141914 416.720654 +L 350.146756 413.043171 +L 348.206305 407.634214 +L 347.42721 404.77704 +L 343.967162 400.347437 +L 343.921882 396.792363 +L 343.665306 393.867949 +L 344.571077 389.734213 +L 346.459385 385.386625 +L 346.821542 383.349324 +L 348.601344 379.03369 +L 349.843048 377.05501 +L 352.750326 373.866828 +L 354.39338 371.697565 +L 355.073666 368.117907 +L 354.952542 365.383804 +L 353.592156 363.682042 +L 352.431888 360.762444 +L 351.356942 357.864713 +L 351.652964 356.84613 +L 353.209339 354.879552 +L 351.893158 350.17752 +L 350.991766 346.908471 +L 348.629437 343.839315 +L 349.115887 342.872353 +L 348.465198 341.354206 +L 347.231957 337.660068 +L 343.282186 332.480513 +L 338.271672 327.544459 +L 335.031543 323.481282 +L 332.042616 318.36568 +L 332.20568 316.701971 +L 333.293008 315.087792 +L 334.498801 311.426125 +L 335.491704 307.695885 +L 334.561206 306.952961 +L 336.235816 301.292916 +L 336.927008 297.316586 +L 334.960635 294.024 +L 332.692035 293.203462 +L 331.660234 290.964728 +L 330.383502 290.260593 +L 330.42348 288.870807 +L 325.292175 290.743087 +L 323.403539 290.49963 +L 321.500154 291.645612 +L 317.51426 291.574431 +L 314.819975 288.452886 +L 313.148695 284.828052 +L 309.592856 281.542196 +L 305.847818 281.627 +L 301.444558 281.644939 +L 297.320619 282.245408 +L 293.299989 283.326754 +L 285.459473 286.272363 +L 282.674371 287.993237 +L 278.159419 289.440436 +L 273.70407 287.996551 +L 271.428613 288.037103 +L 267.948088 287.034701 +L 264.747378 287.071875 +L 258.834539 287.902066 +L 255.364234 289.320217 +L 250.421768 291.110728 +L 249.463421 290.967024 +L 248.161972 290.995578 +L 243.082726 288.533589 +L 238.65372 284.64282 +L 234.513251 281.827371 +L 231.28316 278.529422 +L 229.974771 278.13311 +L 226.514971 276.044897 +L 224.06029 273.305205 +L 223.268867 271.455554 +L 222.801767 267.73616 +L 220.795767 264.726397 +L 219.005049 262.726054 +L 217.803416 262.051253 +L 216.651024 261.029598 +L 216.20997 258.807455 +L 215.564128 257.689757 +L 214.218172 256.838877 +L 211.789473 254.690783 +L 209.812834 254.310819 +L 208.807864 252.877424 +L 208.87947 252.119336 +L 207.50547 251.02448 +L 207.271422 249.950538 +L 206.746887 246.115471 +L 207.494167 243.927066 +L 205.844862 240.015033 +L 203.662654 238.18799 +L 205.775022 237.304529 +L 208.293228 233.895476 +L 209.596077 231.391604 +L 209.41271 228.729064 +L 210.904407 226.33891 +L 211.875943 221.740978 +L 211.812112 216.905956 +L 211.495192 214.473587 +L 212.187465 212.065246 +L 211.252934 209.733863 +L 209.104289 207.58469 +L 209.522222 205.553853 +L 209.984254 203.331231 +L 211.846214 202.067742 +L 213.587002 199.610031 +L 213.485567 197.985132 +L 215.400769 194.662318 +L 218.212664 191.706656 +L 219.787513 190.978963 +L 221.271834 188.251821 +L 221.66917 185.746972 +L 223.5913 182.884496 +L 226.697581 181.245442 +L 230.000136 176.553693 +L 232.466497 174.755831 +L 236.552408 174.32176 +L 240.287296 171.23259 +L 242.573716 170.045347 +L 246.528261 166.284371 +L 245.997162 160.631436 +L 247.979451 156.791046 +L 248.771818 154.448584 +L 251.741365 151.48634 +L 256.08505 149.521041 +L 259.301317 147.736945 +L 262.38539 143.242242 +L 263.855074 140.59362 +L 266.84259 140.639546 +L 269.193171 142.495721 +L 273.095458 142.219964 +L 277.303836 143.198522 +L 279.085812 143.250884 +L 283.067687 140.89322 +L 287.481265 140.147453 +L 290.041302 138.370502 +L 293.91858 137.059488 +L 300.721849 136.273508 +L 307.343586 135.886256 +L 309.405817 136.505449 +L 313.051312 134.790614 +L 317.301697 134.71319 +L 319.01224 135.686572 +L 321.707878 135.393686 +L 325.826673 133.617953 +L 328.647265 134.085592 +L 328.802024 136.242733 +L 331.948492 134.619513 +L 332.340356 135.432355 +L 330.63693 137.559684 +L 330.862324 139.547444 +L 332.384975 140.595649 +L 332.328527 144.358509 +L 329.910939 146.597554 +L 330.96232 148.976934 +L 333.095025 149.018059 +L 334.381614 151.097894 +L 336.034461 151.761665 +L 341.074676 153.197564 +L 342.751656 152.781374 +L 346.289957 153.450192 +L 352.038893 155.31665 +L 354.594723 159.249643 +L 358.439926 160.028566 +L 364.561297 161.759327 +L 369.338499 163.880246 +L 371.115046 162.644604 +L 372.661579 160.50694 +L 371.038222 157.097434 +L 371.85281 154.885902 +L 374.271603 152.7097 +L 376.850701 152.013478 +L 382.355975 152.732643 +L 384.159139 154.66921 +L 385.616681 154.633096 +L 387.040955 155.345456 +L 391.028087 155.715636 +L 392.342051 157.152379 +L 397.451039 156.862608 +L 401.443333 157.882771 +L 405.575182 159.033158 +L 407.51954 159.64938 +L 410.010108 158.086295 +L 411.169069 156.726964 +L 414.3138 156.191633 +L 417.089852 156.617105 +L 418.765818 158.783611 +L 419.161653 157.274363 +L 422.397159 158.169924 +L 425.282644 158.262352 +L 426.672035 157.031459 +L 427.214994 155.499786 +L 426.889098 155.261921 +L 427.114816 153.129003 +L 426.657615 149.751811 +L 426.754216 148.608446 +L 426.833173 148.557863 +L 426.745571 144.92023 +L 427.258695 141.757913 +L 427.263216 141.603572 +L 425.655931 138.32981 +L 425.756698 136.519031 +L 423.758863 134.664378 +L 424.33905 132.994179 +L 422.494396 133.480331 +L 419.338894 132.676688 +L 418.017399 135.25297 +L 413.051318 136.026999 +L 409.425482 133.896374 +L 405.653697 133.949784 +L 405.479769 135.754388 +L 403.230161 136.383489 +L 399.039883 134.282752 +L 395.186153 134.540413 +L 391.6911 130.432225 +L 388.33347 128.221562 +L 388.951989 124.913103 +L 386.065792 123.037185 +L 388.49566 118.957536 +L 393.633833 118.553691 +L 393.817948 115.41661 +L 400.272175 115.632596 +L 402.99137 112.829078 +L 406.135846 111.498745 +L 411.195962 111.111146 +L 417.879397 113.576691 +L 422.96112 114.834548 +L 426.07805 113.975694 +L 428.768533 114.149751 +L 431.001573 111.816393 +L 430.469646 110.094206 +L 428.243664 107.471732 +L 425.81055 106.15772 +L 424.008915 105.826109 +L 422.303266 104.714151 +L 416.863835 101.720004 +L 412.8238 100.501165 +L 409.115168 98.436067 +L 410.824388 97.698716 +L 411.271637 94.417017 +L 408.857067 93.055937 +L 411.898902 91.282002 +L 411.319869 90.487355 +L 409.297174 91.243401 +L 407.31783 91.682768 +L 406.209765 92.993273 +L 403.721652 93.355032 +L 402.076246 94.887179 +L 403.532893 97.202542 +L 405.438919 98.023567 +L 408.222251 97.613233 +L 408.423392 98.995329 +L 405.62895 99.849886 +L 402.790605 102.297806 +L 400.732938 101.615242 +L 400.4716 99.786314 +L 396.656871 98.861046 +L 396.812832 98.107387 +L 398.975134 96.697308 +L 397.675427 95.888613 +L 392.622461 95.198512 +L 391.705355 93.824045 +L 389.204539 94.425405 +L 389.112341 96.537493 +L 388.084523 99.4435 +L 388.610195 100.419593 +L 387.481692 101.314368 +L 386.374081 101.002725 +L 387.525419 105.691166 +L 386.538791 107.383989 +L 386.495017 110.250136 +L 388.457264 112.462698 +L 389.456147 113.978874 +L 392.858649 115.136258 +L 392.647702 116.150758 +L 388.765672 116.557959 +L 387.793552 117.872958 +L 385.733652 120.188036 +L 383.982661 118.340031 +L 383.731981 117.501647 +L 381.604847 117.47453 +L 379.678357 117.165435 +L 375.832282 118.38398 +L 379.001401 120.57814 +L 377.433452 121.314738 +L 375.468519 121.393682 +L 372.945479 119.360049 +L 372.558082 120.279342 +L 374.09708 122.69766 +L 376.471947 124.566091 +L 375.392685 125.524391 +L 377.971368 127.365161 +L 380.121399 128.507278 +L 380.886855 130.875506 +L 377.192696 129.88965 +L 378.889683 131.996294 +L 376.681422 132.526543 +L 379.123991 136.224311 +L 376.662028 136.368031 +L 373.106413 134.622951 +L 370.818882 131.284569 +L 369.428171 128.51353 +L 367.481633 126.64323 +L 364.976319 124.339579 +L 364.415939 123.172088 +L 363.721027 122.903494 +L 363.406618 122.001613 +L 361.034571 120.68836 +L 360.205179 118.755925 +L 359.760782 115.985994 +L 359.899178 114.722242 +L 359.12897 114.108856 +L 358.303805 113.821624 +L 356.956778 112.548232 +L 355.221723 111.802775 +L 351.527317 110.429414 +L 349.12696 109.063885 +L 345.630717 107.978573 +L 342.006401 105.178201 +L 342.637938 104.872133 +L 340.673208 103.282937 +L 340.300912 101.986272 +L 337.953717 101.431704 +L 337.271763 103.1129 +L 335.968479 101.847354 +L 335.752363 100.51855 +L 335.860453 100.454277 +L 336.543479 100.091877 +L 333.702547 99.59516 +L 331.19216 101.00233 +L 331.76193 102.902785 +L 331.55007 104.010326 +L 333.08569 105.963296 +L 336.793434 107.864829 +L 339.261252 111.085764 +L 343.948655 114.20778 +L 346.765192 114.114469 +L 347.846609 114.978934 +L 347.017109 115.805561 +L 350.597965 117.181357 +L 353.552627 118.336087 +L 357.190657 120.371414 +L 357.759444 121.124557 +L 357.441651 122.613329 +L 354.943827 120.756337 +L 351.607272 120.16992 +L 350.657729 122.862232 +L 353.680696 124.321234 +L 353.72636 126.496908 +L 352.215442 126.784591 +L 350.952572 130.425793 +L 349.428056 130.789359 +L 349.18715 129.503617 +L 349.503088 127.242146 +L 350.134766 126.332541 +L 348.167787 123.96674 +L 346.597152 121.918477 +L 344.950763 121.442958 +L 343.49817 119.703735 +L 340.977989 119.01613 +L 339.067215 117.419522 +L 336.294744 117.213089 +L 333.096478 115.447181 +L 329.30896 112.898884 +L 326.471114 110.650652 +L 324.736616 106.780437 +L 322.893855 106.35397 +L 319.813781 105.111307 +L 318.248357 105.65749 +L 316.409525 107.494628 +L 314.94734 107.799606 +L 311.900912 110.05875 +L 304.625188 109.051105 +L 299.378679 110.362702 +L 299.06617 112.757175 +L 299.367886 115.082758 +L 295.906911 117.785539 +L 291.073888 118.654448 +L 290.749631 120.0256 +L 288.41007 122.297765 +L 286.916131 125.660626 +L 288.436495 128.042097 +L 286.157226 129.910785 +L 285.279914 132.652079 +L 282.260511 133.491119 +L 279.348582 136.759426 +L 274.234785 136.802799 +L 270.40446 136.698606 +L 267.796172 138.194089 +L 266.150957 139.806985 +L 264.194396 139.432402 +L 262.806424 137.966811 +L 261.862015 135.495569 +L 258.20061 134.795445 +L 256.496344 135.883028 +L 254.458489 135.257523 +L 252.358656 135.699278 +L 253.327629 132.383393 +L 253.250311 129.787368 +L 251.552851 129.376047 +L 250.819897 127.776455 +L 251.468038 125.057603 +L 253.179246 123.577984 +L 253.653762 121.918046 +L 254.744635 119.468621 +L 254.87489 117.745538 +L 254.313797 116.282705 +L 254.327365 114.923064 +L 254.889522 112.078012 +L 253.639068 110.336065 +L 259.123639 107.562712 +L 263.456557 108.320888 +L 268.321249 108.340877 +L 272.1372 109.041369 +L 275.173241 108.853313 +L 281.059952 109.007368 +L 282.997308 106.659967 +L 283.832997 98.992836 +L 280.381114 95.034689 +L 277.955185 93.145 +L 272.895133 91.696084 +L 272.776297 89.028529 +L 277.115933 88.264929 +L 282.631904 89.219168 +L 281.725279 85.137964 +L 284.76026 86.681233 +L 292.313478 83.894785 +L 293.190274 81.007739 +L 295.923005 80.295091 +L 298.402693 79.597857 +L 299.944967 78.63886 +L 302.190093 73.612441 +L 306.123213 72.179826 +L 308.604974 72.249212 +L 309.078837 71.539993 +L 311.514413 71.33147 +L 312.186204 72.056064 +L 313.887144 70.399024 +L 312.991914 69.178528 +L 312.514541 67.337673 +L 311.031603 65.570831 +L 310.346695 62.343442 +L 310.640931 61.49707 +L 311.233853 60.559051 +L 313.566159 60.33533 +L 314.311694 59.475439 +L 316.23311 58.581845 +L 316.542292 60.154572 +L 315.994537 61.172402 +L 316.530544 62.036828 +L 318.124774 62.48517 +L 317.749924 63.680112 +L 316.847061 63.351382 +L 315.365778 65.66422 +L 316.475376 67.216206 +L 316.799942 68.467319 +L 319.874468 69.180163 +L 320.118701 70.344633 +L 322.904058 69.676268 +L 324.277099 68.750374 +L 327.869739 69.976859 +L 329.537121 70.996163 +L 331.198459 69.985205 +L 335.102462 68.367219 +L 338.196705 67.178774 +L 341.162945 67.646025 +L 341.690732 68.430736 +L 344.370922 68.391054 +L 344.38408 66.948692 +L 347.636588 65.786799 +L 345.81493 63.144841 +L 344.771467 60.799993 +L 345.056847 58.833924 +L 346.866734 57.716872 +L 350.080298 59.914701 +L 352.057261 59.778293 +L 351.197976 57.430284 +L 350.44034 55.657708 +L 349.776352 56.066569 +L 347.635489 55.072849 +L 346.447036 53.396453 +L 348.920317 52.474139 +L 351.556515 51.936632 +L 354.351727 52.304687 +L 356.630941 52.113523 +L 358.012327 50.416167 +L 354.726232 49.185613 +L 350.884151 49.587997 +L 347.605408 50.788953 +L 344.223723 51.53411 +L 341.97267 50.030689 +L 339.216765 49.183576 +L 338.132563 46.439066 +L 335.613776 44.039628 +L 335.684382 42.464875 +L 336.511789 40.767287 +L 339.105882 37.851662 +L 339.986272 37.281398 +L 338.962098 36.253085 +L 335.452537 35.214756 +L 332.707365 36.018951 +L 331.998906 37.798437 +L 333.322051 39.323486 +L 331.318437 41.491358 +L 328.417358 43.876127 +L 328.659269 47.746278 +L 331.24443 49.669438 +L 334.262622 51.17888 +L 333.576289 54.512504 +L 331.294249 55.272916 +L 332.363605 60.364752 +L 331.947507 63.315914 +L 328.564672 63.090507 +L 327.834952 65.635277 +L 324.665056 65.849688 +L 322.920448 62.874709 +L 319.663313 59.404982 +L 316.491018 55.187036 +L 314.301676 53.412505 +L 310.052704 56.905536 +L 306.687436 57.651842 +L 302.819323 56.159934 +L 301.457597 53.002213 +L 299.776893 46.494784 +L 301.647027 44.730646 +L 307.143865 42.433029 +L 310.665591 39.701831 +L 313.189236 36.163039 +L 315.713603 31.507998 +L 317.781794 29.739656 +L 320.651531 26.888832 +L 323.466974 25.857499 +L 326.152004 25.875842 +L 326.892789 24.096492 +L 329.676076 24.073286 +L 331.842443 23.559853 +L 338.049289 24.802921 +L 336.819298 25.446097 +L 339.958208 26.667077 +L 340.628221 25.860025 +L 344.591763 26.986528 +L 349.235388 27.244401 +L 357.97332 29.304436 +L 360.524036 30.270514 +L 362.647629 31.761171 +L 362.534841 33.095355 +L 360.736022 33.901658 +L 351.414358 32.592381 +L 350.522169 32.957336 +L 355.140968 34.516222 +L 359.239512 38.112422 +L 362.3284 38.730979 +L 364.411036 39.293626 +L 363.315901 38.065612 +L 361.024372 37.036476 +L 361.15374 36.078586 +L 366.962011 37.317854 +L 367.638981 36.619864 +L 364.399472 34.934811 +L 365.05526 32.370744 +L 366.600495 32.393799 +L 369.07922 33.08735 +L 367.709849 31.441527 +L 364.61581 30.204102 +L 363.360065 28.834402 +L 360.367 27.584142 +L 365.09159 27.949857 +L 367.727057 29.090521 +L 366.464031 29.512544 +L 368.31541 30.745007 +L 370.534393 31.417648 +L 371.88452 30.734581 +L 370.00526 29.29355 +L 370.926398 28.002734 +L 371.738219 25.763545 +L 372.727733 25.767651 +L 373.956004 27.15488 +L 375.710289 27.210047 +L 375.165459 26.401805 +L 376.973931 26.093713 +L 376.762789 25.048373 +L 380.253931 26.118343 +L 378.581938 24.618006 +L 375.292523 23.614229 +L 374.391122 22.904605 +L 378.066827 23.092153 +L 380.456566 23.506708 +L 387.988684 25.242394 +L 386.155343 24.097864 +L 383.177353 23.215048 +L 382.23213 22.810401 +L 380.990805 22.793656 +L 379.156423 21.847887 +L 375.403557 20.478973 +L 373.987205 19.905096 +L 371.119235 18.084559 +L 367.416946 16.489462 +L 366.914552 16.082363 +L 369.370663 16.183283 +L 372.010138 17.063803 +L 375.248339 18.557165 +L 377.000905 19.027533 +L 380.326557 20.224257 +L 386.375602 22.804585 +L 389.707344 23.823632 +L 392.461187 25.216129 +L 397.550409 28.485046 +L 398.915633 28.6091 +L 397.573162 27.776239 +L 397.064775 27.054296 +L 395.005798 25.989173 +L 393.314483 24.886598 +L 390.468977 23.853441 +L 387.71263 22.507508 +L 386.811869 22.516378 +L 384.181106 21.505864 +L 379.993653 19.568477 +L 375.679897 18.325158 +L 373.36133 16.956888 +L 369.980856 15.836393 +L 370.067551 15.737953 +L 372.840889 16.561156 +L 377.146211 18.221438 +L 378.413263 18.388197 +L 374.901237 17.232793 +L 373.665154 16.36941 +L 374.043383 16.013487 +L 377.200848 16.686406 +L 373.051758 15.473213 +L 367.972743 13.862953 +L 367.376694 13.356694 +L 367.972507 13.128904 +L 367.566719 12.67962 +L 364.973477 12.049683 +L 361.884336 11.037414 +L 361.80971 10.872688 +L 359.427535 10.028671 +L 358.688574 9.580034 +L 357.33286 9.216739 +L 356.63027 8.832641 +L 357.610576 9.007681 +L 354.675728 8.17017 +L 354.512842 8.07114 +L 376.996894 14.095723 +L 405.140153 24.898908 +L 432 38.584684 +L 457.282153 55.003106 +L 480.709615 73.97429 +L 502.02571 95.290385 +L 520.996894 118.717847 +L 537.415316 144 +L 551.101092 170.859847 z -M 451.611024 111.055187 -L 454.641654 113.743953 -L 455.905571 113.811123 -L 457.335195 114.809996 -L 455.502302 115.343234 -L 456.964899 118.517457 -L 457.372173 119.983284 -L 456.98969 121.030694 -L 458.227785 123.051453 -L 460.771381 126.01089 -L 463.704447 126.622804 -L 466.649338 128.523425 -L 470.614644 128.852342 -L 473.755071 127.312507 -L 473.409058 126.314966 -L 471.222049 123.448174 -L 468.922963 119.136743 -L 466.250446 117.978595 -L 465.087394 115.154649 -L 463.399346 115.098676 -L 461.721067 111.68372 -L 464.508963 112.405337 -L 465.60685 110.902055 -L 462.386068 108.736448 -L 460.204332 106.579598 -L 459.096223 107.784162 -L 460.809805 110.686676 -L 458.38238 108.223086 -L 457.604727 107.284959 -L 457.042878 105.602457 -L 455.672799 104.304703 -L 452.124505 103.285514 -L 448.670822 99.96937 -L 446.722598 99.145651 -L 445.763478 97.925177 -L 448.225456 98.047417 -L 446.286977 95.302355 -L 447.692382 94.493521 -L 449.991059 94.834054 -L 447.559886 91.197352 -L 445.385138 88.992438 -L 443.433164 89.405739 -L 440.919006 88.722123 -L 439.637468 90.58547 -L 438.126319 91.565389 -L 438.699912 93.852438 -L 436.872179 94.683937 -L 437.17333 98.773684 -L 441.838143 102.171878 -L 443.309155 104.785469 -L 448.987428 109.110722 +M 451.611024 111.055187 +L 454.641654 113.743953 +L 455.905571 113.811123 +L 457.335195 114.809996 +L 455.502302 115.343234 +L 456.964899 118.517457 +L 457.372173 119.983284 +L 456.98969 121.030694 +L 458.227785 123.051453 +L 460.771381 126.01089 +L 463.704447 126.622804 +L 466.649338 128.523425 +L 470.614644 128.852342 +L 473.755071 127.312507 +L 473.409058 126.314966 +L 471.222049 123.448174 +L 468.922963 119.136743 +L 466.250446 117.978595 +L 465.087394 115.154649 +L 463.399346 115.098676 +L 461.721067 111.68372 +L 464.508963 112.405337 +L 465.60685 110.902055 +L 462.386068 108.736448 +L 460.204332 106.579598 +L 459.096223 107.784162 +L 460.809805 110.686676 +L 458.38238 108.223086 +L 457.604727 107.284959 +L 457.042878 105.602457 +L 455.672799 104.304703 +L 452.124505 103.285514 +L 448.670822 99.96937 +L 446.722598 99.145651 +L 445.763478 97.925177 +L 448.225456 98.047417 +L 446.286977 95.302355 +L 447.692382 94.493521 +L 449.991059 94.834054 +L 447.559886 91.197352 +L 445.385138 88.992438 +L 443.433164 89.405739 +L 440.919006 88.722123 +L 439.637468 90.58547 +L 438.126319 91.565389 +L 438.699912 93.852438 +L 436.872179 94.683937 +L 437.17333 98.773684 +L 441.838143 102.171878 +L 443.309155 104.785469 +L 448.987428 109.110722 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 313.344896 12.313022 -L 311.948299 12.995145 -L 309.752827 12.713788 -L 309.980801 12.314856 -L 308.830105 11.876589 -L 310.414701 11.526165 -L 311.512427 12.042681 + <path clip-path="url(#p9f79fa544f)" d="M 313.344896 12.313022 +L 311.948299 12.995145 +L 309.752827 12.713788 +L 309.980801 12.314856 +L 308.830105 11.876589 +L 310.414701 11.526165 +L 311.512427 12.042681 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 304.125395 9.976601 -L 308.259166 10.878826 -L 306.619041 11.495995 -L 307.241804 12.564741 -L 306.637795 12.86765 -L 307.344181 14.136797 -L 306.071285 14.231606 -L 302.969166 13.350777 -L 303.493031 12.799093 -L 301.615614 12.407869 -L 298.820336 11.243018 -L 297.377977 10.193226 -L 299.383456 9.682439 -L 300.258736 10.128768 -L 301.485164 10.082565 -L 301.362569 9.630939 -L 302.540637 9.555226 + <path clip-path="url(#p9f79fa544f)" d="M 304.125395 9.976601 +L 308.259166 10.878826 +L 306.619041 11.495995 +L 307.241804 12.564741 +L 306.637795 12.86765 +L 307.344181 14.136797 +L 306.071285 14.231606 +L 302.969166 13.350777 +L 303.493031 12.799093 +L 301.615614 12.407869 +L 298.820336 11.243018 +L 297.377977 10.193226 +L 299.383456 9.682439 +L 300.258736 10.128768 +L 301.485164 10.082565 +L 301.362569 9.630939 +L 302.540637 9.555226 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> - <path clip-path="url(#p9f79fa544f)" d="M 273.192434 5.448143 -L 275.025128 6.371501 -L 273.193624 6.738355 -L 270.766146 6.691915 -L 267.345208 6.639683 -L 267.174732 6.844154 -L 269.577672 6.832157 -L 270.713758 7.289576 -L 272.581431 7.001414 -L 272.441937 7.417394 -L 270.571839 8.055948 -L 273.048944 7.688713 -L 276.989402 7.34505 -L 278.830947 7.610473 -L 278.778698 8.11518 -L 274.781706 8.905392 -L 274.005891 9.179782 -L 271.157806 9.322092 -L 272.964405 9.438911 -L 270.839335 10.357462 -L 269.057293 11.214347 -L 267.243235 12.804416 -L 267.449772 13.824024 -L 265.711093 13.836424 -L 263.394344 14.266031 -L 264.522071 15.170066 -L 263.401259 16.571049 -L 261.95767 16.685007 -L 262.141292 18.212538 -L 259.182353 18.246031 -L 259.975627 19.016702 -L 258.942613 19.638827 -L 256.78417 19.856315 -L 254.924715 19.79349 -L 255.354581 21.110899 -L 254.564014 21.957398 -L 252.583982 21.0682 -L 251.342186 21.545581 -L 252.745976 22.097268 -L 253.514691 23.358427 -L 252.657644 24.996516 -L 249.555027 25.290971 -L 249.12343 24.460806 -L 248.453491 23.241387 -L 247.574009 24.617633 -L 244.734884 25.61857 -L 248.713807 25.871907 -L 250.777169 26.065532 -L 244.847633 27.742302 -L 238.792774 29.263361 -L 233.258632 29.79434 -L 231.48184 29.710413 -L 228.87139 30.476685 -L 223.912653 32.749829 -L 218.506327 34.169838 -L 217.25877 34.193456 -L 214.4013 34.628683 -L 211.408731 35.020635 -L 208.20482 36.408016 -L 206.109559 38.128165 -L 203.321398 39.710041 -L 198.164553 41.539687 -L 196.502665 43.637598 -L 193.235173 45.784588 -L 189.390168 48.382199 -L 186.666707 48.336847 -L 186.650093 45.87449 -L 183.345459 45.547307 -L 183.607924 43.928606 -L 185.878804 41.275426 -L 187.692048 37.875086 -L 189.316404 36.207528 -L 192.363605 34.048964 -L 194.007374 31.72113 -L 197.166741 30.122278 -L 197.703292 29.256717 -L 203.191519 26.892817 -L 206.261973 26.30919 -L 208.238843 25.498133 -L 211.328753 23.953463 -L 208.78404 24.522542 -L 207.643808 24.754697 -L 206.15031 24.930674 -L 206.123559 24.125471 -L 208.675557 22.789631 -L 211.109397 21.821839 -L 212.06699 21.902384 -L 213.135446 22.643752 -L 213.787855 21.246599 -L 214.246326 20.514005 -L 212.777221 20.658853 -L 212.995864 20.111747 -L 217.640836 18.581344 -L 218.586634 17.875143 -L 220.690845 16.634966 -L 224.073326 14.821873 -L 224.786707 14.105161 -L 226.531774 13.482101 -L 227.341838 12.389993 -L 226.469353 12.090924 -L 224.919061 11.893242 -L 223.450665 11.75677 -L 224.406316 11.215622 -L 226.636209 10.236417 -L 229.083669 10.081034 -L 230.161683 10.193305 -L 229.676838 9.493368 -L 230.85541 8.811239 -L 232.726695 8.378353 -L 236.171541 8.147223 -L 239.685735 7.894553 -L 241.468201 7.527921 -L 242.161087 6.962763 -L 244.267939 6.620771 -L 248.56631 6.159142 -L 249.545077 6.142786 -L 251.383147 5.698127 -L 253.334751 5.586358 -L 255.103699 5.601057 -L 256.323031 5.743806 -L 255.526558 6.092974 -L 258.804016 5.693507 -L 258.451048 6.158783 -L 258.826495 6.299487 -L 258.710735 6.722246 -L 259.45944 6.075389 -L 261.077595 5.670909 -L 264.653358 5.247662 -L 266.063805 5.409836 -L 267.802815 5.112981 -L 269.675547 5.129423 + <path clip-path="url(#p9f79fa544f)" d="M 273.192434 5.448143 +L 275.025128 6.371501 +L 273.193624 6.738355 +L 270.766146 6.691915 +L 267.345208 6.639683 +L 267.174732 6.844154 +L 269.577672 6.832157 +L 270.713758 7.289576 +L 272.581431 7.001414 +L 272.441937 7.417394 +L 270.571839 8.055948 +L 273.048944 7.688713 +L 276.989402 7.34505 +L 278.830947 7.610473 +L 278.778698 8.11518 +L 274.781706 8.905392 +L 274.005891 9.179782 +L 271.157806 9.322092 +L 272.964405 9.438911 +L 270.839335 10.357462 +L 269.057293 11.214347 +L 267.243235 12.804416 +L 267.449772 13.824024 +L 265.711093 13.836424 +L 263.394344 14.266031 +L 264.522071 15.170066 +L 263.401259 16.571049 +L 261.95767 16.685007 +L 262.141292 18.212538 +L 259.182353 18.246031 +L 259.975627 19.016702 +L 258.942613 19.638827 +L 256.78417 19.856315 +L 254.924715 19.79349 +L 255.354581 21.110899 +L 254.564014 21.957398 +L 252.583982 21.0682 +L 251.342186 21.545581 +L 252.745976 22.097268 +L 253.514691 23.358427 +L 252.657644 24.996516 +L 249.555027 25.290971 +L 249.12343 24.460806 +L 248.453491 23.241387 +L 247.574009 24.617633 +L 244.734884 25.61857 +L 248.713807 25.871907 +L 250.777169 26.065532 +L 244.847633 27.742302 +L 238.792774 29.263361 +L 233.258632 29.79434 +L 231.48184 29.710413 +L 228.87139 30.476685 +L 223.912653 32.749829 +L 218.506327 34.169838 +L 217.25877 34.193456 +L 214.4013 34.628683 +L 211.408731 35.020635 +L 208.20482 36.408016 +L 206.109559 38.128165 +L 203.321398 39.710041 +L 198.164553 41.539687 +L 196.502665 43.637598 +L 193.235173 45.784588 +L 189.390168 48.382199 +L 186.666707 48.336847 +L 186.650093 45.87449 +L 183.345459 45.547307 +L 183.607924 43.928606 +L 185.878804 41.275426 +L 187.692048 37.875086 +L 189.316404 36.207528 +L 192.363605 34.048964 +L 194.007374 31.72113 +L 197.166741 30.122278 +L 197.703292 29.256717 +L 203.191519 26.892817 +L 206.261973 26.30919 +L 208.238843 25.498133 +L 211.328753 23.953463 +L 208.78404 24.522542 +L 207.643808 24.754697 +L 206.15031 24.930674 +L 206.123559 24.125471 +L 208.675557 22.789631 +L 211.109397 21.821839 +L 212.06699 21.902384 +L 213.135446 22.643752 +L 213.787855 21.246599 +L 214.246326 20.514005 +L 212.777221 20.658853 +L 212.995864 20.111747 +L 217.640836 18.581344 +L 218.586634 17.875143 +L 220.690845 16.634966 +L 224.073326 14.821873 +L 224.786707 14.105161 +L 226.531774 13.482101 +L 227.341838 12.389993 +L 226.469353 12.090924 +L 224.919061 11.893242 +L 223.450665 11.75677 +L 224.406316 11.215622 +L 226.636209 10.236417 +L 229.083669 10.081034 +L 230.161683 10.193305 +L 229.676838 9.493368 +L 230.85541 8.811239 +L 232.726695 8.378353 +L 236.171541 8.147223 +L 239.685735 7.894553 +L 241.468201 7.527921 +L 242.161087 6.962763 +L 244.267939 6.620771 +L 248.56631 6.159142 +L 249.545077 6.142786 +L 251.383147 5.698127 +L 253.334751 5.586358 +L 255.103699 5.601057 +L 256.323031 5.743806 +L 255.526558 6.092974 +L 258.804016 5.693507 +L 258.451048 6.158783 +L 258.826495 6.299487 +L 258.710735 6.722246 +L 259.45944 6.075389 +L 261.077595 5.670909 +L 264.653358 5.247662 +L 266.063805 5.409836 +L 267.802815 5.112981 +L 269.675547 5.129423 z " style="fill:#f9f9f9;stroke:#f9f9f9;"/> </g> <g id="PathCollection_2"> - <path clip-path="url(#p9f79fa544f)" d="M 451.611024 111.055187 -L 448.987428 109.110722 -L 443.309155 104.785469 -L 441.838143 102.171878 -L 437.17333 98.773684 -L 436.872179 94.683937 -L 438.699912 93.852438 -L 438.126319 91.565389 -L 439.637468 90.58547 -L 440.919006 88.722123 -L 443.433164 89.405739 -L 445.385138 88.992438 -L 447.559886 91.197352 -L 449.991059 94.834054 -L 447.692382 94.493521 -L 446.286977 95.302355 -L 448.225456 98.047417 -L 445.763478 97.925177 -L 446.722598 99.145651 -L 448.670822 99.96937 -L 452.124505 103.285514 -L 455.672799 104.304703 -L 457.042878 105.602457 -L 457.604727 107.284959 -L 458.38238 108.223086 -L 460.809805 110.686676 -L 459.096223 107.784162 -L 460.204332 106.579598 -L 462.386068 108.736448 -L 465.60685 110.902055 -L 464.508963 112.405337 -L 461.721067 111.68372 -L 463.399346 115.098676 -L 465.087394 115.154649 -L 466.250446 117.978595 -L 468.922963 119.136743 -L 471.222049 123.448174 -L 473.409058 126.314966 -L 473.755071 127.312507 -L 470.614644 128.852342 -L 466.649338 128.523425 -L 463.704447 126.622804 -L 460.771381 126.01089 -L 458.227785 123.051453 -L 456.98969 121.030694 -L 457.372173 119.983284 -L 456.964899 118.517457 -L 455.502302 115.343234 -L 457.335195 114.809996 -L 455.905571 113.811123 -L 454.641654 113.743953 + <path clip-path="url(#p9f79fa544f)" d="M 451.611024 111.055187 +L 448.987428 109.110722 +L 443.309155 104.785469 +L 441.838143 102.171878 +L 437.17333 98.773684 +L 436.872179 94.683937 +L 438.699912 93.852438 +L 438.126319 91.565389 +L 439.637468 90.58547 +L 440.919006 88.722123 +L 443.433164 89.405739 +L 445.385138 88.992438 +L 447.559886 91.197352 +L 449.991059 94.834054 +L 447.692382 94.493521 +L 446.286977 95.302355 +L 448.225456 98.047417 +L 445.763478 97.925177 +L 446.722598 99.145651 +L 448.670822 99.96937 +L 452.124505 103.285514 +L 455.672799 104.304703 +L 457.042878 105.602457 +L 457.604727 107.284959 +L 458.38238 108.223086 +L 460.809805 110.686676 +L 459.096223 107.784162 +L 460.204332 106.579598 +L 462.386068 108.736448 +L 465.60685 110.902055 +L 464.508963 112.405337 +L 461.721067 111.68372 +L 463.399346 115.098676 +L 465.087394 115.154649 +L 466.250446 117.978595 +L 468.922963 119.136743 +L 471.222049 123.448174 +L 473.409058 126.314966 +L 473.755071 127.312507 +L 470.614644 128.852342 +L 466.649338 128.523425 +L 463.704447 126.622804 +L 460.771381 126.01089 +L 458.227785 123.051453 +L 456.98969 121.030694 +L 457.372173 119.983284 +L 456.964899 118.517457 +L 455.502302 115.343234 +L 457.335195 114.809996 +L 455.905571 113.811123 +L 454.641654 113.743953 z " style="fill:#6aaed6;stroke:#6aaed6;"/> - <path clip-path="url(#p9f79fa544f)" d="M 288 0 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 1.093058 -L 288 0 + <path clip-path="url(#p9f79fa544f)" d="M 288 0 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 1.093058 +L 288 0 z " style="fill:#6aaed6;stroke:#6aaed6;"/> - <path clip-path="url(#p9f79fa544f)" d="M 253.81942 573.77667 -L 253.769035 573.73312 -L 252.0135 573.437716 -L 250.470066 573.165162 -L 248.096028 572.736722 -L 245.708927 572.352299 -L 243.845736 572.091836 -L 243.571768 572.153602 -L 228.121433 569.706509 -L 218.019911 566.999814 -L 217.400376 566.803144 -L 215.272236 566.171108 -L 213.402498 565.641212 -L 211.7008 565.123963 -L 209.921729 564.594219 -L 205.960085 563.300866 -L 204.174405 562.727071 -L 202.410978 562.175596 -L 200.536451 561.54654 -L 198.804326 560.944248 -L 196.831376 560.271163 -L 194.902728 559.66031 -L 193.262649 559.158568 -L 191.522233 558.560669 -L 189.502015 557.811324 -L 188.047868 557.192038 -L 186.740822 556.530255 -L 185.926169 556.061889 -L 185.022068 555.545459 -L 185.548188 555.549855 -L 185.228428 555.227267 -L 183.427697 554.537197 -L 181.857679 553.828143 -L 180.401805 553.101855 -L 179.702131 552.613252 -L 179.423145 552.234919 -L 179.213249 551.88512 -L 179.720671 551.827072 -L 181.157706 552.257956 -L 180.80956 551.9034 -L 180.867534 551.485474 -L 180.049467 550.877555 -L 178.373337 550.262661 -L 177.890795 550.371721 -L 178.441197 550.912662 -L 178.616909 551.30384 -L 178.180716 551.401682 -L 178.209506 551.674023 -L 178.817332 552.188642 -L 178.099744 552.144133 -L 178.531692 552.538005 -L 179.142597 552.978858 -L 180.564078 553.739195 -L 181.082972 554.163034 -L 181.820786 554.637621 -L 182.870585 555.22029 -L 183.693862 555.668177 -L 185.25523 556.399542 -L 187.024964 557.107796 -L 189.02197 557.881607 -L 190.602243 558.521237 -L 192.116937 559.147027 -L 193.428545 559.686272 -L 195.082213 560.299854 -L 196.840964 560.923021 -L 198.551685 561.500186 -L 200.470608 562.131786 -L 202.412957 562.754002 -L 203.761349 563.179244 -L 202.600062 562.868078 -L 200.593752 562.21201 -L 198.43688 561.4984 -L 196.330317 560.788414 -L 194.140855 560.037834 -L 170.859847 551.101092 -L 144 537.415316 -L 118.717847 520.996894 -L 95.290385 502.02571 -L 73.97429 480.709615 -L 55.003106 457.282153 -L 38.584684 432 -L 24.898908 405.140153 -L 14.095723 376.996894 -L 6.293491 347.878567 -L 1.577694 318.104197 -L 0 288 -L 1.577694 257.895803 -L 6.293491 228.121433 -L 7.718398 222.803609 -L 7.66332 223.307593 -L 7.735784 223.626607 -L 8.02754 222.525665 -L 7.987811 223.079451 -L 7.669031 224.679846 -L 7.572499 224.99216 -L 7.706831 224.186396 -L 7.50948 224.894933 -L 7.122241 227.316992 -L 6.896144 229.036009 -L 6.608525 230.783805 -L 6.350341 232.964915 -L 6.164694 234.66216 -L 5.89775 235.51461 -L 5.867054 236.330401 -L 5.527932 237.825906 -L 5.253358 239.324868 -L 5.127684 240.378891 -L 5.16749 241.00516 -L 5.027789 242.47418 -L 5.314892 241.378967 -L 5.538115 239.915867 -L 5.499486 240.910758 -L 5.281402 242.385041 -L 5.339388 243.165963 -L 5.379393 244.307836 -L 5.338033 245.612228 -L 5.186503 246.722765 -L 4.99143 247.533593 -L 4.947757 248.662526 -L 5.105111 249.996732 -L 5.068735 250.796704 -L 5.241917 249.732845 -L 5.517778 249.909459 -L 5.608126 250.601777 -L 5.847784 251.108527 -L 5.719086 253.193364 -L 5.926965 253.62742 -L 6.140604 252.83827 -L 5.887892 255.883375 -L 6.2966 255.8293 -L 6.511238 255.153809 -L 6.826666 254.634853 -L 6.789783 251.732587 -L 7.018025 250.740197 -L 7.226254 250.660648 -L 7.759206 249.586217 -L 8.202013 247.946001 -L 8.652829 247.816678 -L 8.91933 249.407461 -L 8.861728 251.126944 -L 9.068248 251.574396 -L 8.631856 252.796979 -L 8.487953 255.572602 -L 8.650136 257.150123 -L 8.764313 259.976386 -L 8.483427 264.262401 -L 8.121791 265.481048 -L 7.966826 270.147356 -L 7.543263 272.968925 -L 7.86844 274.316772 -L 7.286025 276.778434 -L 6.674337 279.7436 -L 6.148755 279.866603 -L 5.838598 281.580538 -L 5.795586 284.107227 -L 5.420426 284.323513 -L 5.510311 285.936077 -L 4.822315 287.633559 -L 4.319344 288.471195 -L 4.359996 290.544646 -L 4.034376 293.606905 -L 3.913661 296.64633 -L 3.643751 297.2492 -L 3.897446 301.863946 -L 3.787028 303.185957 -L 4.368334 305.636025 -L 4.597314 303.478625 -L 4.880197 305.755512 -L 4.613551 309.260061 -L 4.170789 312.069263 -L 4.199718 315.427832 -L 5.002632 320.387214 -L 4.980224 322.467608 -L 5.765483 324.786 -L 6.815331 328.357457 -L 7.577428 332.140118 -L 8.276745 334.568426 -L 10.804637 344.776902 -L 13.62284 354.215943 -L 16.009394 360.944039 -L 16.152945 362.271333 -L 17.625504 366.430591 -L 19.330281 369.711542 -L 23.113219 375.67338 -L 27.383762 381.238822 -L 28.106521 383.215096 -L 30.521738 386.31723 -L 33.030834 392.996865 -L 35.867297 400.613089 -L 39.425474 410.708294 -L 43.046311 419.912204 -L 46.902967 428.376321 -L 48.818525 433.466886 -L 51.879629 438.805978 -L 53.465972 442.199625 -L 57.528419 448.557243 -L 60.857084 454.544088 -L 64.343492 460.769785 -L 68.205188 466.955323 -L 67.816838 466.941729 -L 71.23239 471.345948 -L 74.498032 475.172751 -L 76.090492 477.607172 -L 81.58264 484.365985 -L 85.938777 489.432057 -L 87.040655 490.126897 -L 83.283232 485.737797 -L 84.870506 486.908426 -L 91.155358 494.101157 -L 88.853536 492.534973 -L 94.21296 498.183829 -L 96.464019 500.895321 -L 98.857382 502.310298 -L 100.658868 504.564729 -L 103.786673 507.575879 -L 110.202462 513.009729 -L 113.112657 515.196126 -L 115.106466 516.902546 -L 117.778824 518.883244 -L 121.005222 520.89144 -L 124.687071 523.197564 -L 126.976933 524.404021 -L 127.292199 524.44178 -L 123.889556 521.79813 -L 123.450433 520.970181 -L 123.043261 520.36901 -L 124.05072 520.613672 -L 124.715294 520.866951 -L 121.826197 518.991414 -L 117.661029 515.780836 -L 116.437223 514.452768 -L 116.114667 513.472105 -L 112.804239 509.963292 -L 112.424944 508.477299 -L 109.859947 505.664402 -L 107.855361 504.741356 -L 104.068923 502.043902 -L 101.952936 499.603473 -L 101.356986 498.083814 -L 102.710664 498.341479 -L 101.336886 496.573202 -L 98.36934 493.11973 -L 97.688315 491.160815 -L 98.232766 490.325733 -L 96.166366 488.383734 -L 96.230774 489.35299 -L 94.235548 488.076436 -L 91.00993 484.433611 -L 90.850419 483.591263 -L 93.516937 485.208923 -L 94.855977 485.007723 -L 94.88536 483.898062 -L 93.102031 481.988069 -L 91.339455 479.267057 -L 89.284001 476.964028 -L 91.596024 477.673635 -L 94.925575 477.484018 -L 96.546836 475.890902 -L 95.313288 471.265156 -L 94.199463 469.388125 -L 91.735705 467.503495 -L 90.338351 464.833726 -L 85.469066 461.042093 -L 84.361795 458.967626 -L 87.02588 461.374035 -L 88.460209 461.452549 -L 91.585486 463.440854 -L 92.5817 463.176726 -L 94.787083 464.187848 -L 96.21491 462.310481 -L 95.859661 459.92194 -L 96.260136 457.81039 -L 95.384269 454.073268 -L 96.191149 452.353649 -L 96.941826 449.331787 -L 96.628085 442.278812 -L 97.620757 440.146148 -L 97.403511 438.13015 -L 96.185261 433.841039 -L 94.750447 431.398908 -L 93.939876 428.184053 -L 94.918674 424.070685 -L 97.371636 420.884327 -L 100.523404 419.905405 -L 102.185409 418.118937 -L 106.900812 416.836534 -L 110.609118 417.143441 -L 110.64276 414.51204 -L 112.928835 412.77324 -L 112.289979 408.128999 -L 114.470165 402.417045 -L 113.727248 396.277883 -L 114.504003 394.524713 -L 114.26724 391.472255 -L 113.930642 384.291693 -L 112.158391 375.361986 -L 112.684207 371.911509 -L 113.627772 371.883941 -L 115.901105 367.908057 -L 117.700346 362.608079 -L 122.570176 356.202158 -L 124.319633 353.141137 -L 125.267693 345.130331 -L 124.374932 342.109231 -L 122.595651 335.732982 -L 121.032611 334.079062 -L 117.55972 333.660892 -L 114.397163 332.029077 -L 109.08662 326.121165 -L 103.190402 321.590782 -L 97.512995 321.358699 -L 90.219731 318.14638 -L 86.066722 319.330151 -L 86.572013 316.618144 -L 84.761213 313.540957 -L 78.888033 309.966673 -L 74.573118 307.764296 -L 72.068077 310.798636 -L 71.8996 305.771605 -L 66.12804 304.396451 -L 65.135773 302.781602 -L 67.584825 298.910584 -L 67.562309 295.441792 -L 65.864185 294.434601 -L 64.413981 285.47342 -L 63.784895 282.616258 -L 62.707824 282.736341 -L 62.317038 280.625963 -L 59.370824 276.030234 -L 57.25841 274.587875 -L 56.299274 273.917272 -L 53.29 272.189932 -L 50.965623 272.261266 -L 50.587646 273.124812 -L 47.371258 271.687018 -L 46.464485 269.799829 -L 45.275992 267.162828 -L 44.299013 266.904432 -L 44.498198 264.321211 -L 43.275132 260.837998 -L 41.840187 258.758198 -L 41.016321 257.435886 -L 39.72266 257.352133 -L 39.884571 253.303621 -L 38.437342 250.571185 -L 36.634844 249.892877 -L 36.228292 247.419096 -L 38.419561 246.277713 -L 35.673218 245.88953 -L 32.911809 245.710066 -L 32.679209 246.953913 -L 31.237322 248.285489 -L 29.753686 247.372368 -L 28.971627 244.927532 -L 26.801549 245.013948 -L 25.128632 244.612513 -L 25.344183 242.958327 -L 24.678249 239.940812 -L 23.468427 239.579077 -L 23.535897 235.96927 -L 22.639544 237.382403 -L 22.421628 239.764824 -L 20.026075 241.267113 -L 19.442835 245.034235 -L 19.645912 246.904237 -L 18.718782 250.398917 -L 17.990237 250.544947 -L 18.030352 246.452983 -L 19.19921 243.757257 -L 19.681782 241.17548 -L 19.599643 238.801121 -L 20.620797 238.463862 -L 20.892806 237.310318 -L 21.531809 235.729353 -L 21.38342 234.328112 -L 20.896448 233.883074 -L 19.674862 236.056993 -L 18.876494 236.999502 -L 17.261448 239.167465 -L 16.264237 238.433158 -L 15.96189 239.428643 -L 15.118201 239.261132 -L 13.970163 241.314522 -L 13.014872 245.418208 -L 12.733744 247.053314 -L 12.151086 247.408183 -L 10.745986 250.557205 -L 10.19602 250.181197 -L 10.000626 248.650277 -L 9.894549 247.02064 -L 9.584064 245.975996 -L 9.07385 245.570054 -L 9.188963 245.099485 -L 8.729069 244.574113 -L 8.184624 245.909729 -L 7.512821 246.649808 -L 6.977072 247.720092 -L 6.547999 247.867688 -L 6.537297 246.531619 -L 6.406776 246.894659 -L 6.166323 246.500457 -L 6.346986 245.460064 -L 6.401928 243.530364 -L 6.471221 241.219072 -L 6.600544 239.066645 -L 6.961908 236.279318 -L 7.036147 235.405294 -L 7.274676 234.055859 -L 7.639147 232.885529 -L 7.870834 231.559234 -L 8.369744 229.50019 -L 8.55909 229.08377 -L 9.037251 226.86853 -L 9.297138 225.585274 -L 9.823023 223.447232 -L 10.360926 221.530696 -L 10.907413 219.969583 -L 11.306081 218.147627 -L 11.619556 217.085464 -L 11.792682 216.655832 -L 12.001568 215.212494 -L 12.007414 214.318088 -L 12.159347 213.112141 -L 12.263912 212.081695 -L 12.219987 211.907823 -L 12.208577 211.047251 -L 12.007067 211.381758 -L 11.87132 211.385892 -L 11.875301 210.957323 -L 11.835202 210.573582 -L 11.641916 211.064455 -L 11.389577 211.462787 -L 11.224053 211.394577 -L 11.235107 210.763836 -L 11.13371 210.937808 -L 11.226543 210.505237 -L 11.152642 210.454176 -L 10.875742 211.209563 -L 10.912767 210.977623 -L 10.963487 210.69277 -L 11.639251 208.170784 -L 12.015016 207.056372 -L 12.764521 204.681119 -L 12.89654 204.196538 -L 13.446771 202.528439 -L 13.668635 201.752387 -L 14.359239 199.756991 -L 14.788895 198.469005 -L 14.76776 198.368464 -L 15.000463 197.668691 -L 15.058377 197.67674 -L 14.694315 199.003692 -L 15.99512 195.401609 -L 16.742065 193.416825 -L 16.996992 192.518654 -L 18.089479 189.745908 -L 19.264423 187.174399 -L 20.142933 184.934134 -L 20.471848 183.846334 -L 20.185272 183.994134 -L 20.112015 183.471038 -L 20.011979 183.590734 -L 24.898908 170.859847 -L 36.406857 148.274226 -L 36.394774 148.331301 -L 36.86094 147.645961 -L 36.51065 148.432144 -L 36.843009 147.930683 -L 37.319515 146.986841 -L 38.329998 145.227885 -L 39.03246 143.932161 -L 39.405869 143.487838 -L 39.649871 143.478555 -L 39.50592 144.292022 -L 40.083963 144.174438 -L 39.644284 145.484605 -L 38.642716 147.701387 -L 38.584693 147.986895 -L 40.128937 146.388534 -L 39.925889 147.20442 -L 38.264732 151.166088 -L 37.097115 153.700072 -L 35.417325 156.567798 -L 34.594868 158.370061 -L 33.130079 161.979674 -L 31.566949 166.069623 -L 31.323684 167.261874 -L 30.496969 169.320626 -L 30.594522 170.066399 -L 31.084267 169.607342 -L 32.578647 166.92566 -L 33.456687 165.180018 -L 34.973351 162.148159 -L 37.267444 156.773215 -L 38.277424 154.85088 -L 39.65674 151.552518 -L 41.531294 147.674325 -L 43.162741 144.58488 -L 45.084784 141.571655 -L 46.945093 139.199308 -L 48.597244 137.371933 -L 51.163204 135.010513 -L 52.186457 133.642615 -L 53.601701 132.276016 -L 54.200209 132.184059 -L 56.401226 129.957063 -L 58.197183 129.082576 -L 60.913015 126.220381 -L 63.642406 122.057918 -L 64.549229 120.612019 -L 64.475513 120.233059 -L 67.260018 116.375181 -L 67.607028 114.893615 -L 67.730855 115.703393 -L 69.459187 113.102112 -L 70.963562 111.453909 -L 68.533833 114.791976 -L 67.872409 116.493316 -L 65.548529 119.132937 -L 65.519282 119.324936 -L 68.448049 116.293699 -L 69.939478 114.844528 -L 71.055128 113.33282 -L 71.363949 112.545304 -L 72.816487 110.351073 -L 72.367275 111.443853 -L 72.427583 111.721312 -L 71.716924 112.763179 -L 74.834255 109.962074 -L 77.297448 107.223212 -L 77.132989 106.950613 -L 78.323348 105.972763 -L 77.90604 106.438289 -L 78.595986 106.633743 -L 81.125352 105.913902 -L 81.374588 105.088523 -L 79.149493 105.351999 -L 80.97367 104.498481 -L 81.787454 104.491045 -L 82.45096 104.43453 -L 83.886006 103.994047 -L 84.417326 104.217631 -L 85.781592 103.803111 -L 86.783801 102.731751 -L 87.104027 101.791408 -L 86.083657 103.218931 -L 85.638008 102.995441 -L 86.914406 100.872677 -L 88.630571 98.869766 -L 89.314872 98.287327 -L 92.144514 96.012482 -L 94.445822 95.224656 -L 96.926127 94.246441 -L 99.968052 92.769391 -L 100.800941 91.519217 -L 102.763963 91.389349 -L 105.242722 91.717352 -L 99.993654 94.269102 -L 97.344735 97.430903 -L 98.24863 97.926054 -L 102.242964 95.553693 -L 105.094885 94.33036 -L 110.643248 92.743176 -L 114.836778 90.676122 -L 114.853314 89.191316 -L 117.041523 86.566459 -L 111.777611 90.352618 -L 108.628078 90.432276 -L 108.247312 88.270394 -L 110.255356 85.48332 -L 113.420705 81.606961 -L 117.051611 79.52855 -L 117.480269 77.617633 -L 115.119246 77.567019 -L 109.560056 79.863086 -L 103.082413 84.00189 -L 101.548109 84.341731 -L 106.070319 81.429276 -L 112.428931 77.267964 -L 115.566559 76.103155 -L 119.168916 73.910536 -L 120.773649 73.943569 -L 122.814631 74.347109 -L 125.23898 75.588232 -L 128.435683 75.45509 -L 133.224711 73.006717 -L 137.156983 72.211209 -L 139.541683 71.216651 -L 142.032287 70.146383 -L 145.620357 66.500423 -L 146.208818 65.215252 -L 145.380458 64.627527 -L 147.646167 61.900471 -L 147.757375 60.78138 -L 146.289863 59.64989 -L 147.060673 57.732469 -L 147.317928 55.771775 -L 150.166139 54.01885 -L 153.255482 50.359496 -L 156.61755 46.57169 -L 159.258327 44.026964 -L 156.72045 45.13149 -L 151.363576 47.926887 -L 147.684075 49.156115 -L 149.312393 47.379207 -L 149.082221 46.760965 -L 153.916382 43.320375 -L 157.392271 41.151872 -L 156.258279 40.603456 -L 157.650573 39.563025 -L 159.20821 37.88097 -L 160.029453 36.872181 -L 158.378583 37.349067 -L 158.181679 36.909278 -L 158.432115 35.915824 -L 157.083809 36.331645 -L 150.50174 40.252534 -L 146.876324 42.66446 -L 141.812809 45.166179 -L 139.331143 47.470445 -L 136.230326 49.97391 -L 133.545718 51.879166 -L 130.358953 53.747116 -L 126.832947 55.515434 -L 124.060082 56.501144 -L 122.322257 58.246032 -L 116.375503 63.207883 -L 112.054048 66.299794 -L 110.358559 67.130123 -L 113.300102 63.730861 -L 117.414011 60.089983 -L 121.329098 56.988247 -L 124.914048 54.483049 -L 124.919245 53.932911 -L 124.677824 53.355952 -L 126.183203 51.887887 -L 127.122615 50.801221 -L 129.014959 49.304913 -L 130.539797 48.014977 -L 132.363747 46.393489 -L 131.688042 46.580163 -L 131.688041 46.580163 -L 137.154941 43.029917 -L 138.964732 41.906858 -L 139.3042 41.634172 -L 139.304201 41.634172 -L 141.153679 40.433106 -L 144.947137 38.246069 -L 148.311855 36.408976 -L 153.105588 33.969664 -L 156.586207 32.353377 -L 157.0836 32.30094 -L 159.999784 30.854093 -L 161.89581 30.074392 -L 162.239951 30.202839 -L 165.397732 28.950261 -L 167.418284 28.06989 -L 171.392324 26.481592 -L 173.740054 25.509494 -L 172.530652 26.303332 -L 173.528802 26.229913 -L 177.245108 25.146475 -L 179.512801 24.200236 -L 181.783339 23.043591 -L 184.449789 22.147621 -L 186.734255 21.213101 -L 188.713874 20.106045 -L 189.105495 19.620954 -L 189.242835 19.272284 -L 184.088817 21.238511 -L 179.971408 22.739857 -L 176.50786 23.963261 -L 179.569569 22.500095 -L 183.04789 21.162315 -L 186.013759 19.809512 -L 182.335032 21.013819 -L 187.132945 19.182365 -L 188.17003 18.547304 -L 190.435608 17.843986 -L 195.868058 15.805049 -L 198.063667 14.975955 -L 198.991197 14.551407 -L 195.739182 15.507324 -L 192.32115 16.660686 -L 199.003106 14.095723 -L 200.670099 13.649054 -L 199.127055 14.175271 -L 197.14719 14.848473 -L 196.515742 15.110744 -L 198.000978 14.741972 -L 202.245245 13.424947 -L 204.581452 12.614685 -L 206.690061 12.065467 -L 207.664458 11.774921 -L 217.132761 9.237897 -L 217.715431 9.100303 -L 220.200465 8.504729 -L 222.569532 7.912938 -L 221.947211 8.048812 -L 222.489773 7.845104 -L 222.075261 7.913558 -L 222.075261 7.913558 -L 228.121433 6.293491 -L 231.815044 5.708481 -L 231.289444 5.87382 -L 229.373956 6.321549 -L 228.523746 6.575925 -L 230.614615 6.141755 -L 233.057644 5.641064 -L 234.730193 5.246766 -L 234.730194 5.246766 -L 257.895803 1.577694 -L 288 0 -L 318.104197 1.577694 -L 340.122404 5.065035 -L 340.063038 5.066866 -L 340.762066 5.22718 -L 347.451704 6.583832 -L 344.492903 5.757255 -L 344.492901 5.757254 -L 347.878567 6.293491 -L 350.066061 6.879628 -L 350.060307 6.884019 -L 348.494931 6.512738 -L 347.448787 6.355432 -L 349.994888 6.97857 -L 352.252415 7.510761 -L 353.403271 7.773831 -L 354.512842 8.07114 -L 354.675728 8.17017 -L 357.610576 9.007681 -L 356.63027 8.832641 -L 357.33286 9.216739 -L 358.688574 9.580034 -L 359.427535 10.028671 -L 361.80971 10.872688 -L 361.884336 11.037414 -L 364.973477 12.049683 -L 367.566719 12.67962 -L 367.972507 13.128904 -L 367.376694 13.356694 -L 367.972743 13.862953 -L 373.051758 15.473213 -L 377.200848 16.686406 -L 374.043383 16.013487 -L 373.665154 16.36941 -L 374.901237 17.232793 -L 378.413263 18.388197 -L 377.146211 18.221438 -L 372.840889 16.561156 -L 370.067551 15.737953 -L 369.980856 15.836393 -L 373.36133 16.956888 -L 375.679897 18.325158 -L 379.993653 19.568477 -L 384.181106 21.505864 -L 386.811869 22.516378 -L 387.71263 22.507508 -L 390.468977 23.853441 -L 393.314483 24.886598 -L 395.005798 25.989173 -L 397.064775 27.054296 -L 397.573162 27.776239 -L 398.915633 28.6091 -L 397.550409 28.485046 -L 392.461187 25.216129 -L 389.707344 23.823632 -L 386.375602 22.804585 -L 380.326557 20.224257 -L 377.000905 19.027533 -L 375.248339 18.557165 -L 372.010138 17.063803 -L 369.370663 16.183283 -L 366.914552 16.082363 -L 367.416946 16.489462 -L 371.119235 18.084559 -L 373.987205 19.905096 -L 375.403557 20.478973 -L 379.156423 21.847887 -L 380.990805 22.793656 -L 382.23213 22.810401 -L 383.177353 23.215048 -L 386.155343 24.097864 -L 387.988684 25.242394 -L 380.456566 23.506708 -L 378.066827 23.092153 -L 374.391122 22.904605 -L 375.292523 23.614229 -L 378.581938 24.618006 -L 380.253931 26.118343 -L 376.762789 25.048373 -L 376.973931 26.093713 -L 375.165459 26.401805 -L 375.710289 27.210047 -L 373.956004 27.15488 -L 372.727733 25.767651 -L 371.738219 25.763545 -L 370.926398 28.002734 -L 370.00526 29.29355 -L 371.88452 30.734581 -L 370.534393 31.417648 -L 368.31541 30.745007 -L 366.464031 29.512544 -L 367.727057 29.090521 -L 365.09159 27.949857 -L 360.367 27.584142 -L 363.360065 28.834402 -L 364.61581 30.204102 -L 367.709849 31.441527 -L 369.07922 33.08735 -L 366.600495 32.393799 -L 365.05526 32.370744 -L 364.399472 34.934811 -L 367.638981 36.619864 -L 366.962011 37.317854 -L 361.15374 36.078586 -L 361.024372 37.036476 -L 363.315901 38.065612 -L 364.411036 39.293626 -L 362.3284 38.730979 -L 359.239512 38.112422 -L 355.140968 34.516222 -L 350.522169 32.957336 -L 351.414358 32.592381 -L 360.736022 33.901658 -L 362.534841 33.095355 -L 362.647629 31.761171 -L 360.524036 30.270514 -L 357.97332 29.304436 -L 349.235388 27.244401 -L 344.591763 26.986528 -L 340.628221 25.860025 -L 339.958208 26.667077 -L 336.819298 25.446097 -L 338.049289 24.802921 -L 331.842443 23.559853 -L 329.676076 24.073286 -L 326.892789 24.096492 -L 326.152004 25.875842 -L 323.466974 25.857499 -L 320.651531 26.888832 -L 317.781794 29.739656 -L 315.713603 31.507998 -L 313.189236 36.163039 -L 310.665591 39.701831 -L 307.143865 42.433029 -L 301.647027 44.730646 -L 299.776893 46.494784 -L 301.457597 53.002213 -L 302.819323 56.159934 -L 306.687436 57.651842 -L 310.052704 56.905536 -L 314.301676 53.412505 -L 316.491018 55.187036 -L 319.663313 59.404982 -L 322.920448 62.874709 -L 324.665056 65.849688 -L 327.834952 65.635277 -L 328.564672 63.090507 -L 331.947507 63.315914 -L 332.363605 60.364752 -L 331.294249 55.272916 -L 333.576289 54.512504 -L 334.262622 51.17888 -L 331.24443 49.669438 -L 328.659269 47.746278 -L 328.417358 43.876127 -L 331.318437 41.491358 -L 333.322051 39.323486 -L 331.998906 37.798437 -L 332.707365 36.018951 -L 335.452537 35.214756 -L 338.962098 36.253085 -L 339.986272 37.281398 -L 339.105882 37.851662 -L 336.511789 40.767287 -L 335.684382 42.464875 -L 335.613776 44.039628 -L 338.132563 46.439066 -L 339.216765 49.183576 -L 341.97267 50.030689 -L 344.223723 51.53411 -L 347.605408 50.788953 -L 350.884151 49.587997 -L 354.726232 49.185613 -L 358.012327 50.416167 -L 356.630941 52.113523 -L 354.351727 52.304687 -L 351.556515 51.936632 -L 348.920317 52.474139 -L 346.447036 53.396453 -L 347.635489 55.072849 -L 349.776352 56.066569 -L 350.44034 55.657708 -L 351.197976 57.430284 -L 352.057261 59.778293 -L 350.080298 59.914701 -L 346.866734 57.716872 -L 345.056847 58.833924 -L 344.771467 60.799993 -L 345.81493 63.144841 -L 347.636588 65.786799 -L 344.38408 66.948692 -L 344.370922 68.391054 -L 341.690732 68.430736 -L 341.162945 67.646025 -L 338.196705 67.178774 -L 335.102462 68.367219 -L 331.198459 69.985205 -L 329.537121 70.996163 -L 327.869739 69.976859 -L 324.277099 68.750374 -L 322.904058 69.676268 -L 320.118701 70.344633 -L 319.874468 69.180163 -L 316.799942 68.467319 -L 316.475376 67.216206 -L 315.365778 65.66422 -L 316.847061 63.351382 -L 317.749924 63.680112 -L 318.124774 62.48517 -L 316.530544 62.036828 -L 315.994537 61.172402 -L 316.542292 60.154572 -L 316.23311 58.581845 -L 314.311694 59.475439 -L 313.566159 60.33533 -L 311.233853 60.559051 -L 310.640931 61.49707 -L 310.346695 62.343442 -L 311.031603 65.570831 -L 312.514541 67.337673 -L 312.991914 69.178528 -L 313.887144 70.399024 -L 312.186204 72.056064 -L 311.514413 71.33147 -L 309.078837 71.539993 -L 308.604974 72.249212 -L 306.123213 72.179826 -L 302.190093 73.612441 -L 299.944967 78.63886 -L 298.402693 79.597857 -L 295.923005 80.295091 -L 293.190274 81.007739 -L 292.313478 83.894785 -L 284.76026 86.681233 -L 281.725279 85.137964 -L 282.631904 89.219168 -L 277.115933 88.264929 -L 272.776297 89.028529 -L 272.895133 91.696084 -L 277.955185 93.145 -L 280.381114 95.034689 -L 283.832997 98.992836 -L 282.997308 106.659967 -L 281.059952 109.007368 -L 275.173241 108.853313 -L 272.1372 109.041369 -L 268.321249 108.340877 -L 263.456557 108.320888 -L 259.123639 107.562712 -L 253.639068 110.336065 -L 254.889522 112.078012 -L 254.327365 114.923064 -L 254.313797 116.282705 -L 254.87489 117.745538 -L 254.744635 119.468621 -L 253.653762 121.918046 -L 253.179246 123.577984 -L 251.468038 125.057603 -L 250.819897 127.776455 -L 251.552851 129.376047 -L 253.250311 129.787368 -L 253.327629 132.383393 -L 252.358656 135.699278 -L 254.458489 135.257523 -L 256.496344 135.883028 -L 258.20061 134.795445 -L 261.862015 135.495569 -L 262.806424 137.966811 -L 264.194396 139.432402 -L 266.150957 139.806985 -L 267.796172 138.194089 -L 270.40446 136.698606 -L 274.234785 136.802799 -L 279.348582 136.759426 -L 282.260511 133.491119 -L 285.279914 132.652079 -L 286.157226 129.910785 -L 288.436495 128.042097 -L 286.916131 125.660626 -L 288.41007 122.297765 -L 290.749631 120.0256 -L 291.073888 118.654448 -L 295.906911 117.785539 -L 299.367886 115.082758 -L 299.06617 112.757175 -L 299.378679 110.362702 -L 304.625188 109.051105 -L 311.900912 110.05875 -L 314.94734 107.799606 -L 316.409525 107.494628 -L 318.248357 105.65749 -L 319.813781 105.111307 -L 322.893855 106.35397 -L 324.736616 106.780437 -L 326.471114 110.650652 -L 329.30896 112.898884 -L 333.096478 115.447181 -L 336.294744 117.213089 -L 339.067215 117.419522 -L 340.977989 119.01613 -L 343.49817 119.703735 -L 344.950763 121.442958 -L 346.597152 121.918477 -L 348.167787 123.96674 -L 350.134766 126.332541 -L 349.503088 127.242146 -L 349.18715 129.503617 -L 349.428056 130.789359 -L 350.952572 130.425793 -L 352.215442 126.784591 -L 353.72636 126.496908 -L 353.680696 124.321234 -L 350.657729 122.862232 -L 351.607272 120.16992 -L 354.943827 120.756337 -L 357.441651 122.613329 -L 357.759444 121.124557 -L 357.190657 120.371414 -L 353.552627 118.336087 -L 350.597965 117.181357 -L 347.017109 115.805561 -L 347.846609 114.978934 -L 346.765192 114.114469 -L 343.948655 114.20778 -L 339.261252 111.085764 -L 336.793434 107.864829 -L 333.08569 105.963296 -L 331.55007 104.010326 -L 331.76193 102.902785 -L 331.19216 101.00233 -L 333.702547 99.59516 -L 336.543479 100.091877 -L 335.860453 100.454277 -L 335.752363 100.51855 -L 335.968479 101.847354 -L 337.271763 103.1129 -L 337.953717 101.431704 -L 340.300912 101.986272 -L 340.673208 103.282937 -L 342.637938 104.872133 -L 342.006401 105.178201 -L 345.630717 107.978573 -L 349.12696 109.063885 -L 351.527317 110.429414 -L 355.221723 111.802775 -L 356.956778 112.548232 -L 358.303805 113.821624 -L 359.12897 114.108856 -L 359.899178 114.722242 -L 359.760782 115.985994 -L 360.205179 118.755925 -L 361.034571 120.68836 -L 363.406618 122.001613 -L 363.721027 122.903494 -L 364.415939 123.172088 -L 364.976319 124.339579 -L 367.481633 126.64323 -L 369.428171 128.51353 -L 370.818882 131.284569 -L 373.106413 134.622951 -L 376.662028 136.368031 -L 379.123991 136.224311 -L 376.681422 132.526543 -L 378.889683 131.996294 -L 377.192696 129.88965 -L 380.886855 130.875506 -L 380.121399 128.507278 -L 377.971368 127.365161 -L 375.392685 125.524391 -L 376.471947 124.566091 -L 374.09708 122.69766 -L 372.558082 120.279342 -L 372.945479 119.360049 -L 375.468519 121.393682 -L 377.433452 121.314738 -L 379.001401 120.57814 -L 375.832282 118.38398 -L 379.678357 117.165435 -L 381.604847 117.47453 -L 383.731981 117.501647 -L 383.982661 118.340031 -L 385.733652 120.188036 -L 387.793552 117.872958 -L 388.765672 116.557959 -L 392.647702 116.150758 -L 392.858649 115.136258 -L 389.456147 113.978874 -L 388.457264 112.462698 -L 386.495017 110.250136 -L 386.538791 107.383989 -L 387.525419 105.691166 -L 386.374081 101.002725 -L 387.481692 101.314368 -L 388.610195 100.419593 -L 388.084523 99.4435 -L 389.112341 96.537493 -L 389.204539 94.425405 -L 391.705355 93.824045 -L 392.622461 95.198512 -L 397.675427 95.888613 -L 398.975134 96.697308 -L 396.812832 98.107387 -L 396.656871 98.861046 -L 400.4716 99.786314 -L 400.732938 101.615242 -L 402.790605 102.297806 -L 405.62895 99.849886 -L 408.423392 98.995329 -L 408.222251 97.613233 -L 405.438919 98.023567 -L 403.532893 97.202542 -L 402.076246 94.887179 -L 403.721652 93.355032 -L 406.209765 92.993273 -L 407.31783 91.682768 -L 409.297174 91.243401 -L 411.319869 90.487355 -L 411.898902 91.282002 -L 408.857067 93.055937 -L 411.271637 94.417017 -L 410.824388 97.698716 -L 409.115168 98.436067 -L 412.8238 100.501165 -L 416.863835 101.720004 -L 422.303266 104.714151 -L 424.008915 105.826109 -L 425.81055 106.15772 -L 428.243664 107.471732 -L 430.469646 110.094206 -L 431.001573 111.816393 -L 428.768533 114.149751 -L 426.07805 113.975694 -L 422.96112 114.834548 -L 417.879397 113.576691 -L 411.195962 111.111146 -L 406.135846 111.498745 -L 402.99137 112.829078 -L 400.272175 115.632596 -L 393.817948 115.41661 -L 393.633833 118.553691 -L 388.49566 118.957536 -L 386.065792 123.037185 -L 388.951989 124.913103 -L 388.33347 128.221562 -L 391.6911 130.432225 -L 395.186153 134.540413 -L 399.039883 134.282752 -L 403.230161 136.383489 -L 405.479769 135.754388 -L 405.653697 133.949784 -L 409.425482 133.896374 -L 413.051318 136.026999 -L 418.017399 135.25297 -L 419.338894 132.676688 -L 422.494396 133.480331 -L 424.33905 132.994179 -L 423.758863 134.664378 -L 425.756698 136.519031 -L 425.655931 138.32981 -L 427.263216 141.603572 -L 427.258695 141.757913 -L 426.745571 144.92023 -L 426.833173 148.557863 -L 426.754216 148.608446 -L 426.657615 149.751811 -L 427.114816 153.129003 -L 426.889098 155.261921 -L 427.214994 155.499786 -L 426.672035 157.031459 -L 425.282644 158.262352 -L 422.397159 158.169924 -L 419.161653 157.274363 -L 418.765818 158.783611 -L 417.089852 156.617105 -L 414.3138 156.191633 -L 411.169069 156.726964 -L 410.010108 158.086295 -L 407.51954 159.64938 -L 405.575182 159.033158 -L 401.443333 157.882771 -L 397.451039 156.862608 -L 392.342051 157.152379 -L 391.028087 155.715636 -L 387.040955 155.345456 -L 385.616681 154.633096 -L 384.159139 154.66921 -L 382.355975 152.732643 -L 376.850701 152.013478 -L 374.271603 152.7097 -L 371.85281 154.885902 -L 371.038222 157.097434 -L 372.661579 160.50694 -L 371.115046 162.644604 -L 369.338499 163.880246 -L 364.561297 161.759327 -L 358.439926 160.028566 -L 354.594723 159.249643 -L 352.038893 155.31665 -L 346.289957 153.450192 -L 342.751656 152.781374 -L 341.074676 153.197564 -L 336.034461 151.761665 -L 334.381614 151.097894 -L 333.095025 149.018059 -L 330.96232 148.976934 -L 329.910939 146.597554 -L 332.328527 144.358509 -L 332.384975 140.595649 -L 330.862324 139.547444 -L 330.63693 137.559684 -L 332.340356 135.432355 -L 331.948492 134.619513 -L 328.802024 136.242733 -L 328.647265 134.085592 -L 325.826673 133.617953 -L 321.707878 135.393686 -L 319.01224 135.686572 -L 317.301697 134.71319 -L 313.051312 134.790614 -L 309.405817 136.505449 -L 307.343586 135.886256 -L 300.721849 136.273508 -L 293.91858 137.059488 -L 290.041302 138.370502 -L 287.481265 140.147453 -L 283.067687 140.89322 -L 279.085812 143.250884 -L 277.303836 143.198522 -L 273.095458 142.219964 -L 269.193171 142.495721 -L 266.84259 140.639546 -L 263.855074 140.59362 -L 262.38539 143.242242 -L 259.301317 147.736945 -L 256.08505 149.521041 -L 251.741365 151.48634 -L 248.771818 154.448584 -L 247.979451 156.791046 -L 245.997162 160.631436 -L 246.528261 166.284371 -L 242.573716 170.045347 -L 240.287296 171.23259 -L 236.552408 174.32176 -L 232.466497 174.755831 -L 230.000136 176.553693 -L 226.697581 181.245442 -L 223.5913 182.884496 -L 221.66917 185.746972 -L 221.271834 188.251821 -L 219.787513 190.978963 -L 218.212664 191.706656 -L 215.400769 194.662318 -L 213.485567 197.985132 -L 213.587002 199.610031 -L 211.846214 202.067742 -L 209.984254 203.331231 -L 209.522222 205.553853 -L 209.104289 207.58469 -L 211.252934 209.733863 -L 212.187465 212.065246 -L 211.495192 214.473587 -L 211.812112 216.905956 -L 211.875943 221.740978 -L 210.904407 226.33891 -L 209.41271 228.729064 -L 209.596077 231.391604 -L 208.293228 233.895476 -L 205.775022 237.304529 -L 203.662654 238.18799 -L 205.844862 240.015033 -L 207.494167 243.927066 -L 206.746887 246.115471 -L 207.271422 249.950538 -L 207.50547 251.02448 -L 208.87947 252.119336 -L 208.807864 252.877424 -L 209.812834 254.310819 -L 211.789473 254.690783 -L 214.218172 256.838877 -L 215.564128 257.689757 -L 216.20997 258.807455 -L 216.651024 261.029598 -L 217.803416 262.051253 -L 219.005049 262.726054 -L 220.795767 264.726397 -L 222.801767 267.73616 -L 223.268867 271.455554 -L 224.06029 273.305205 -L 226.514971 276.044897 -L 229.974771 278.13311 -L 231.28316 278.529422 -L 234.513251 281.827371 -L 238.65372 284.64282 -L 243.082726 288.533589 -L 248.161972 290.995578 -L 249.463421 290.967024 -L 250.421768 291.110728 -L 255.364234 289.320217 -L 258.834539 287.902066 -L 264.747378 287.071875 -L 267.948088 287.034701 -L 271.428613 288.037103 -L 273.70407 287.996551 -L 278.159419 289.440436 -L 282.674371 287.993237 -L 285.459473 286.272363 -L 293.299989 283.326754 -L 297.320619 282.245408 -L 301.444558 281.644939 -L 305.847818 281.627 -L 309.592856 281.542196 -L 313.148695 284.828052 -L 314.819975 288.452886 -L 317.51426 291.574431 -L 321.500154 291.645612 -L 323.403539 290.49963 -L 325.292175 290.743087 -L 330.42348 288.870807 -L 330.383502 290.260593 -L 331.660234 290.964728 -L 332.692035 293.203462 -L 334.960635 294.024 -L 336.927008 297.316586 -L 336.235816 301.292916 -L 334.561206 306.952961 -L 335.491704 307.695885 -L 334.498801 311.426125 -L 333.293008 315.087792 -L 332.20568 316.701971 -L 332.042616 318.36568 -L 335.031543 323.481282 -L 338.271672 327.544459 -L 343.282186 332.480513 -L 347.231957 337.660068 -L 348.465198 341.354206 -L 349.115887 342.872353 -L 348.629437 343.839315 -L 350.991766 346.908471 -L 351.893158 350.17752 -L 353.209339 354.879552 -L 351.652964 356.84613 -L 351.356942 357.864713 -L 352.431888 360.762444 -L 353.592156 363.682042 -L 354.952542 365.383804 -L 355.073666 368.117907 -L 354.39338 371.697565 -L 352.750326 373.866828 -L 349.843048 377.05501 -L 348.601344 379.03369 -L 346.821542 383.349324 -L 346.459385 385.386625 -L 344.571077 389.734213 -L 343.665306 393.867949 -L 343.921882 396.792363 -L 343.967162 400.347437 -L 347.42721 404.77704 -L 348.206305 407.634214 -L 350.146756 413.043171 -L 352.141914 416.720654 -L 353.713755 418.532082 -L 354.031862 420.956699 -L 353.54173 426.257918 -L 354.212472 432.961734 -L 354.884662 436.070663 -L 355.271933 440.222634 -L 356.505387 443.285228 -L 359.176583 446.389987 -L 361.27592 451.722564 -L 361.280604 451.733235 -L 362.722538 455.160212 -L 364.653069 458.89167 -L 364.118793 461.976609 -L 362.666115 462.741838 -L 363.399785 465.369118 -L 362.867127 467.68094 -L 363.155887 468.719103 -L 363.465553 468.170765 -L 364.758 469.865313 -L 366.065519 469.896825 -L 367.377888 471.231142 -L 369.166748 471.082631 -L 371.938439 469.535553 -L 375.406084 468.8059 -L 379.803801 467.117661 -L 381.339737 467.265009 -L 383.79754 466.698321 -L 387.703547 467.295151 -L 389.80135 466.470407 -L 391.913333 466.956865 -L 392.736778 465.842282 -L 394.715359 465.554597 -L 399.111132 463.8435 -L 402.507337 461.898792 -L 405.90997 459.363584 -L 411.459045 454.96468 -L 414.503969 451.913144 -L 416.208704 449.731984 -L 418.447179 447.532536 -L 419.363628 446.888867 -L 422.561121 444.63034 -L 424.106305 442.671109 -L 425.598726 439.112403 -L 427.43984 435.937447 -L 428.405052 433.646498 -L 427.515952 433.409111 -L 427.689327 431.598564 -L 429.795588 429.892552 -L 434.993118 427.257744 -L 438.496509 425.58355 -L 440.484812 423.916471 -L 441.534042 422.037076 -L 440.850779 421.330976 -L 441.995668 419.222624 -L 443.20501 414.815428 -L 442.479781 415.082244 -L 442.758523 413.735985 -L 442.630467 411.127638 -L 441.581654 407.792679 -L 442.612995 404.509185 -L 444.434853 403.349461 -L 447.813462 399.904861 -L 449.467532 398.96541 -L 454.792604 393.692551 -L 459.503462 391.132875 -L 463.27079 389.108195 -L 466.193436 386.017059 -L 468.238056 382.627747 -L 469.943991 379.151699 -L 469.6954 376.865793 -L 470.737504 369.385453 -L 470.881048 365.187406 -L 471.670321 360.347814 -L 471.328553 358.213367 -L 470.066024 357.250599 -L 469.017846 352.551802 -L 467.981748 349.578322 -L 468.452134 347.216471 -L 468.362845 345.738855 -L 469.672504 342.690379 -L 469.657814 341.417266 -L 467.311109 339.794842 -L 467.272849 337.010645 -L 469.429042 330.779001 -L 471.070959 329.027824 -L 471.973514 325.665226 -L 473.287654 323.567054 -L 473.948233 320.015444 -L 475.390874 319.543408 -L 476.385311 317.390417 -L 479.0741 315.194296 -L 479.943669 313.945162 -L 480.842393 311.241749 -L 484.914739 304.853165 -L 488.276602 300.760224 -L 493.512757 295.319759 -L 496.874633 290.944564 -L 500.576463 283.724504 -L 503.078603 277.832677 -L 505.303033 270.208444 -L 506.65861 263.615914 -L 507.537758 257.878667 -L 507.710864 252.398823 -L 508.112122 250.531469 -L 507.711778 247.921079 -L 507.54572 245.003189 -L 507.253196 243.640546 -L 506.080242 243.780594 -L 504.871949 245.633874 -L 503.27198 246.308105 -L 501.910976 247.195315 -L 500.895073 247.397244 -L 499.068211 247.755739 -L 498.028394 248.776128 -L 496.431802 249.260993 -L 493.6921 251.083707 -L 490.04462 252.010535 -L 486.923264 253.571674 -L 485.166901 253.704497 -L 483.290808 251.754571 -L 482.315307 249.755933 -L 481.023187 248.935008 -L 479.286611 247.700721 -L 481.171067 246.3459 -L 480.981212 244.276667 -L 479.903637 242.81099 -L 477.90844 241.463822 -L 476.512907 239.853103 -L 474.088121 237.155525 -L 471.593579 234.49882 -L 465.756853 230.222927 -L 463.289912 227.977766 -L 461.438468 223.552777 -L 458.176203 218.055363 -L 455.761946 216.397845 -L 454.08528 215.298488 -L 451.502171 209.607597 -L 449.873291 204.684672 -L 450.498118 203.759524 -L 448.205316 199.14174 -L 447.318417 198.207338 -L 441.930215 194.214618 -L 441.053976 191.143386 -L 441.591227 190.274089 -L 436.907827 185.270722 -L 435.016896 182.699284 -L 432.965621 180.246264 -L 428.180477 173.198963 -L 424.598451 168.7168 -L 421.673063 164.000886 -L 421.93053 163.569749 -L 426.463681 169.948138 -L 428.715843 171.898823 -L 430.369026 173.311668 -L 430.999281 172.457656 -L 431.302337 170.008748 -L 431.048781 166.513823 -L 431.496643 164.627257 -L 431.820985 165.27609 -L 431.934333 167.118627 -L 432.257444 168.72522 -L 432.437382 171.273028 -L 434.242671 171.139968 -L 437.022891 174.184638 -L 440.30988 177.773558 -L 442.70766 181.140149 -L 444.058231 182.112118 -L 445.737691 184.470425 -L 445.829375 185.542494 -L 447.750802 188.155508 -L 449.970154 188.997561 -L 452.154475 190.722165 -L 455.591863 195.831836 -L 456.149757 198.666096 -L 457.382069 201.958448 -L 460.865698 206.360924 -L 462.660029 207.033704 -L 465.906732 210.167543 -L 467.797054 214.034734 -L 470.577606 217.961785 -L 472.780957 219.563336 -L 473.455672 221.486573 -L 474.822905 222.872449 -L 475.697095 224.923262 -L 476.26777 227.04792 -L 476.016714 228.028454 -L 476.797936 230.242548 -L 476.128144 230.539682 -L 477.515784 232.479304 -L 478.844803 236.068475 -L 479.670398 237.477355 -L 480.007827 240.188986 -L 481.385591 243.004478 -L 483.873224 243.052461 -L 484.889817 242.284158 -L 486.630889 242.244086 -L 486.965229 240.938682 -L 487.810372 240.497915 -L 488.344538 239.124875 -L 489.159419 238.766858 -L 491.952037 238.25214 -L 493.910014 237.102384 -L 495.464186 234.871121 -L 496.495702 235.068025 -L 497.869643 234.655954 -L 500.058064 230.896602 -L 504.541166 228.084557 -L 507.09246 225.685895 -L 506.794034 224.00989 -L 506.874335 221.776718 -L 508.679063 220.225042 -L 509.941212 219.793932 -L 511.438647 217.92532 -L 513.091248 218.196268 -L 514.087475 216.68112 -L 513.574293 214.722197 -L 514.31831 213.367049 -L 515.993869 213.19171 -L 516.326608 212.09154 -L 515.93989 209.705012 -L 517.056037 207.686622 -L 518.237461 207.542308 -L 518.306099 206.917681 -L 517.051563 203.75176 -L 516.71959 201.269407 -L 516.88624 200.058581 -L 518.088844 200.150372 -L 517.957106 196.757398 -L 518.471212 195.091327 -L 518.405793 193.705703 -L 518.294697 190.761768 -L 517.929085 189.709173 -L 516.878084 189.238121 -L 515.683585 187.769654 -L 513.633701 185.237514 -L 511.8931 184.587301 -L 509.869427 184.224468 -L 507.853917 182.71715 -L 505.533768 179.687818 -L 503.778244 176.07448 -L 503.775038 175.194751 -L 503.249146 173.262862 -L 502.779918 172.904508 -L 502.678223 174.565313 -L 502.181044 177.617543 -L 501.358511 180.83542 -L 500.675183 184.2102 -L 498.986431 184.267273 -L 496.660332 184.408789 -L 494.71426 185.393679 -L 494.166132 184.121926 -L 493.739154 184.405178 -L 492.576659 182.679717 -L 492.217427 179.873553 -L 491.17485 177.164831 -L 489.782794 175.808638 -L 489.194548 176.38935 -L 489.312991 178.901015 -L 490.706293 182.265083 -L 489.873964 181.169514 -L 488.947648 179.684943 -L 487.64886 178.472107 -L 486.717663 176.954394 -L 486.440523 175.385771 -L 485.555715 173.503131 -L 482.853583 171.775398 -L 481.741392 170.217159 -L 479.903347 169.317361 -L 477.22264 165.504384 -L 474.910715 162.185012 -L 474.752314 161.131441 -L 473.324217 159.207509 -L 475.136872 159.259123 -L 475.466447 157.4062 -L 477.903361 158.706126 -L 479.100999 157.824414 -L 483.700047 163.602392 -L 487.315734 167.706997 -L 490.478046 168.711135 -L 494.604118 171.911875 -L 498.428813 173.04744 -L 500.11337 170.504107 -L 501.692657 169.438972 -L 503.207033 170.092321 -L 506.547141 175.582811 -L 509.499088 175.804035 -L 512.468196 176.496001 -L 517.236342 177.241599 -L 519.959583 176.099948 -L 523.195169 175.436734 -L 526.304598 173.906845 -L 529.18191 177.135909 -L 531.081087 180.356493 -L 532.829219 181.259488 -L 536.361909 184.7771 -L 537.55341 186.503022 -L 537.411142 188.381242 -L 541.613134 193.595682 -L 542.903018 193.890013 -L 543.996151 190.498012 -L 545.869251 194.847338 -L 547.835805 200.607669 -L 550.211315 206.615136 -L 553.504889 215.801782 -L 556.472541 222.109448 -L 557.4447 225.087628 -L 559.166459 231.112409 -L 560.827066 235.615814 -L 561.717907 237.801208 -L 563.099928 242.622183 -L 564.781631 249.366033 -L 566.506119 253.604825 -L 566.731084 252.006117 -L 566.57609 248.486478 -L 567.239628 246.693686 -L 566.687763 245.190868 -L 566.464126 241.224605 -L 566.883265 240.766078 -L 565.253296 232.393714 -L 564.591164 227.555835 -L 563.593368 223.523175 -L 561.808577 217.280217 -L 561.045562 213.459748 -L 561.3442 213.010316 -L 561.511855 210.905971 -L 561.501891 209.510819 -L 560.841293 207.296874 -L 560.476592 203.74388 -L 559.906495 200.418702 -L 558.510731 194.366729 -L 557.867986 190.597554 -L 556.97257 187.597879 -L 555.598044 184.101504 -L 555.455913 182.630794 -L 555.547571 182.443322 -L 555.547571 182.443322 -L 561.904277 199.003106 -L 569.706509 228.121433 -L 574.422306 257.895803 -L 576 288 -L 576 288 -L 574.422306 318.104197 -L 569.706509 347.878567 -L 561.904277 376.996894 -L 551.101092 405.140153 -L 537.415316 432 -L 520.996894 457.282153 -L 502.02571 480.709615 -L 480.709615 502.02571 -L 457.282153 520.996894 -L 432 537.415316 -L 405.140153 551.101092 -L 387.991878 557.683698 -L 388.933106 557.288269 -L 388.238444 557.441112 -L 387.863706 557.454663 -L 387.651475 557.393929 -L 387.373279 557.338272 -L 387.499299 557.105282 -L 385.052831 557.944921 -L 383.547189 558.395112 -L 382.937394 558.435007 -L 383.297724 558.08934 -L 383.770394 557.716402 -L 383.05999 557.687321 -L 383.52261 557.299837 -L 383.892914 556.844016 -L 385.456541 556.09021 -L 385.61499 555.713284 -L 384.906152 555.674218 -L 384.086402 555.696692 -L 382.685003 555.972334 -L 380.882265 556.396828 -L 379.145825 556.860587 -L 377.100096 557.457597 -L 375.591523 558.092897 -L 373.722683 558.600982 -L 372.596461 558.68975 -L 370.811231 559.230581 -L 368.435769 559.921349 -L 367.609754 559.844528 -L 365.851791 560.287179 -L 363.980652 560.758602 -L 362.230915 561.187125 -L 360.021488 561.621682 -L 358.259049 561.937821 -L 355.809525 562.548949 -L 354.039051 562.901893 -L 352.259624 563.563366 -L 350.17997 563.995696 -L 349.90233 563.704426 -L 349.922283 563.262486 -L 348.216855 563.472514 -L 347.608982 563.20974 -L 347.978277 562.725536 -L 346.819913 562.585549 -L 345.092121 563.12905 -L 342.863721 563.967104 -L 341.037467 564.415327 -L 339.282164 564.631954 -L 337.456905 564.945729 -L 335.503598 565.380754 -L 333.661428 565.611919 -L 331.867381 565.860291 -L 330.147714 565.958879 -L 328.424535 566.030222 -L 326.550643 566.151535 -L 324.538921 566.440366 -L 324.065023 566.098342 -L 323.904979 565.687672 -L 322.277582 565.667277 -L 320.656112 565.567827 -L 318.954214 565.5929 -L 316.962946 565.698521 -L 315.027186 565.899611 -L 313.20715 566.415435 -L 313.01691 565.94768 -L 310.896427 565.918344 -L 308.908911 566.309514 -L 307.776423 566.82219 -L 305.746081 567.098889 -L 305.122242 566.672348 -L 304.291408 566.085861 -L 302.434127 566.28838 -L 301.33862 565.972066 -L 300.091273 566.441844 -L 298.5257 566.731214 -L 296.591615 566.944487 -L 294.817766 567.24794 -L 292.946479 567.425785 -L 291.066927 567.59884 -L 289.398789 567.813542 -L 287.637955 568.2061 -L 286.933155 567.720374 -L 285.087104 567.646404 -L 283.084817 567.77974 -L 281.067888 567.977046 -L 279.138648 567.893318 -L 278.553705 567.443115 -L 276.748571 567.306609 -L 276.15819 567.77122 -L 276.324749 568.20832 -L 274.429162 568.139847 -L 273.412714 567.736572 -L 271.46713 567.637908 -L 270.567236 567.945362 -L 270.250982 568.476866 -L 269.460206 568.894943 -L 268.297097 569.208091 -L 266.98771 569.426083 -L 265.760453 569.598678 -L 265.245549 569.900389 -L 265.32393 570.301662 -L 267.045768 570.566295 -L 267.169365 570.928634 -L 266.364434 571.168739 -L 265.740496 571.437503 -L 264.530272 571.661791 -L 263.615049 571.821477 -L 262.616291 571.971027 -L 261.579842 572.082345 -L 260.221563 572.132275 -L 258.623644 572.096486 -L 258.056744 572.128308 -L 256.937059 572.174674 -L 255.935346 572.247449 -L 255.9783 572.503576 -L 255.541837 572.661469 -L 255.173185 572.812497 -L 254.299892 572.892235 -L 253.727363 572.980051 -L 253.968487 573.101609 -L 256.008538 573.565279 -L 257.291603 573.795422 -L 258.77335 573.884714 -L 259.959874 573.886455 -L 261.4208 573.945035 -L 262.331865 574.147176 -L 263.52393 574.34369 -L 264.898451 574.537722 -L 264.393551 574.59807 -L 263.305936 574.594542 -L 262.291991 574.574744 -L 261.706333 574.592848 -L 261.298135 574.600615 -L 261.298135 574.600615 -L 260.456438 574.556503 -L 259.735748 574.505457 -L 259.775571 574.52082 -L 258.274705 574.442163 -L 257.594847 574.374639 + <path clip-path="url(#p9f79fa544f)" d="M 253.81942 573.77667 +L 253.769035 573.73312 +L 252.0135 573.437716 +L 250.470066 573.165162 +L 248.096028 572.736722 +L 245.708927 572.352299 +L 243.845736 572.091836 +L 243.571768 572.153602 +L 228.121433 569.706509 +L 218.019911 566.999814 +L 217.400376 566.803144 +L 215.272236 566.171108 +L 213.402498 565.641212 +L 211.7008 565.123963 +L 209.921729 564.594219 +L 205.960085 563.300866 +L 204.174405 562.727071 +L 202.410978 562.175596 +L 200.536451 561.54654 +L 198.804326 560.944248 +L 196.831376 560.271163 +L 194.902728 559.66031 +L 193.262649 559.158568 +L 191.522233 558.560669 +L 189.502015 557.811324 +L 188.047868 557.192038 +L 186.740822 556.530255 +L 185.926169 556.061889 +L 185.022068 555.545459 +L 185.548188 555.549855 +L 185.228428 555.227267 +L 183.427697 554.537197 +L 181.857679 553.828143 +L 180.401805 553.101855 +L 179.702131 552.613252 +L 179.423145 552.234919 +L 179.213249 551.88512 +L 179.720671 551.827072 +L 181.157706 552.257956 +L 180.80956 551.9034 +L 180.867534 551.485474 +L 180.049467 550.877555 +L 178.373337 550.262661 +L 177.890795 550.371721 +L 178.441197 550.912662 +L 178.616909 551.30384 +L 178.180716 551.401682 +L 178.209506 551.674023 +L 178.817332 552.188642 +L 178.099744 552.144133 +L 178.531692 552.538005 +L 179.142597 552.978858 +L 180.564078 553.739195 +L 181.082972 554.163034 +L 181.820786 554.637621 +L 182.870585 555.22029 +L 183.693862 555.668177 +L 185.25523 556.399542 +L 187.024964 557.107796 +L 189.02197 557.881607 +L 190.602243 558.521237 +L 192.116937 559.147027 +L 193.428545 559.686272 +L 195.082213 560.299854 +L 196.840964 560.923021 +L 198.551685 561.500186 +L 200.470608 562.131786 +L 202.412957 562.754002 +L 203.761349 563.179244 +L 202.600062 562.868078 +L 200.593752 562.21201 +L 198.43688 561.4984 +L 196.330317 560.788414 +L 194.140855 560.037834 +L 170.859847 551.101092 +L 144 537.415316 +L 118.717847 520.996894 +L 95.290385 502.02571 +L 73.97429 480.709615 +L 55.003106 457.282153 +L 38.584684 432 +L 24.898908 405.140153 +L 14.095723 376.996894 +L 6.293491 347.878567 +L 1.577694 318.104197 +L 0 288 +L 1.577694 257.895803 +L 6.293491 228.121433 +L 7.718398 222.803609 +L 7.66332 223.307593 +L 7.735784 223.626607 +L 8.02754 222.525665 +L 7.987811 223.079451 +L 7.669031 224.679846 +L 7.572499 224.99216 +L 7.706831 224.186396 +L 7.50948 224.894933 +L 7.122241 227.316992 +L 6.896144 229.036009 +L 6.608525 230.783805 +L 6.350341 232.964915 +L 6.164694 234.66216 +L 5.89775 235.51461 +L 5.867054 236.330401 +L 5.527932 237.825906 +L 5.253358 239.324868 +L 5.127684 240.378891 +L 5.16749 241.00516 +L 5.027789 242.47418 +L 5.314892 241.378967 +L 5.538115 239.915867 +L 5.499486 240.910758 +L 5.281402 242.385041 +L 5.339388 243.165963 +L 5.379393 244.307836 +L 5.338033 245.612228 +L 5.186503 246.722765 +L 4.99143 247.533593 +L 4.947757 248.662526 +L 5.105111 249.996732 +L 5.068735 250.796704 +L 5.241917 249.732845 +L 5.517778 249.909459 +L 5.608126 250.601777 +L 5.847784 251.108527 +L 5.719086 253.193364 +L 5.926965 253.62742 +L 6.140604 252.83827 +L 5.887892 255.883375 +L 6.2966 255.8293 +L 6.511238 255.153809 +L 6.826666 254.634853 +L 6.789783 251.732587 +L 7.018025 250.740197 +L 7.226254 250.660648 +L 7.759206 249.586217 +L 8.202013 247.946001 +L 8.652829 247.816678 +L 8.91933 249.407461 +L 8.861728 251.126944 +L 9.068248 251.574396 +L 8.631856 252.796979 +L 8.487953 255.572602 +L 8.650136 257.150123 +L 8.764313 259.976386 +L 8.483427 264.262401 +L 8.121791 265.481048 +L 7.966826 270.147356 +L 7.543263 272.968925 +L 7.86844 274.316772 +L 7.286025 276.778434 +L 6.674337 279.7436 +L 6.148755 279.866603 +L 5.838598 281.580538 +L 5.795586 284.107227 +L 5.420426 284.323513 +L 5.510311 285.936077 +L 4.822315 287.633559 +L 4.319344 288.471195 +L 4.359996 290.544646 +L 4.034376 293.606905 +L 3.913661 296.64633 +L 3.643751 297.2492 +L 3.897446 301.863946 +L 3.787028 303.185957 +L 4.368334 305.636025 +L 4.597314 303.478625 +L 4.880197 305.755512 +L 4.613551 309.260061 +L 4.170789 312.069263 +L 4.199718 315.427832 +L 5.002632 320.387214 +L 4.980224 322.467608 +L 5.765483 324.786 +L 6.815331 328.357457 +L 7.577428 332.140118 +L 8.276745 334.568426 +L 10.804637 344.776902 +L 13.62284 354.215943 +L 16.009394 360.944039 +L 16.152945 362.271333 +L 17.625504 366.430591 +L 19.330281 369.711542 +L 23.113219 375.67338 +L 27.383762 381.238822 +L 28.106521 383.215096 +L 30.521738 386.31723 +L 33.030834 392.996865 +L 35.867297 400.613089 +L 39.425474 410.708294 +L 43.046311 419.912204 +L 46.902967 428.376321 +L 48.818525 433.466886 +L 51.879629 438.805978 +L 53.465972 442.199625 +L 57.528419 448.557243 +L 60.857084 454.544088 +L 64.343492 460.769785 +L 68.205188 466.955323 +L 67.816838 466.941729 +L 71.23239 471.345948 +L 74.498032 475.172751 +L 76.090492 477.607172 +L 81.58264 484.365985 +L 85.938777 489.432057 +L 87.040655 490.126897 +L 83.283232 485.737797 +L 84.870506 486.908426 +L 91.155358 494.101157 +L 88.853536 492.534973 +L 94.21296 498.183829 +L 96.464019 500.895321 +L 98.857382 502.310298 +L 100.658868 504.564729 +L 103.786673 507.575879 +L 110.202462 513.009729 +L 113.112657 515.196126 +L 115.106466 516.902546 +L 117.778824 518.883244 +L 121.005222 520.89144 +L 124.687071 523.197564 +L 126.976933 524.404021 +L 127.292199 524.44178 +L 123.889556 521.79813 +L 123.450433 520.970181 +L 123.043261 520.36901 +L 124.05072 520.613672 +L 124.715294 520.866951 +L 121.826197 518.991414 +L 117.661029 515.780836 +L 116.437223 514.452768 +L 116.114667 513.472105 +L 112.804239 509.963292 +L 112.424944 508.477299 +L 109.859947 505.664402 +L 107.855361 504.741356 +L 104.068923 502.043902 +L 101.952936 499.603473 +L 101.356986 498.083814 +L 102.710664 498.341479 +L 101.336886 496.573202 +L 98.36934 493.11973 +L 97.688315 491.160815 +L 98.232766 490.325733 +L 96.166366 488.383734 +L 96.230774 489.35299 +L 94.235548 488.076436 +L 91.00993 484.433611 +L 90.850419 483.591263 +L 93.516937 485.208923 +L 94.855977 485.007723 +L 94.88536 483.898062 +L 93.102031 481.988069 +L 91.339455 479.267057 +L 89.284001 476.964028 +L 91.596024 477.673635 +L 94.925575 477.484018 +L 96.546836 475.890902 +L 95.313288 471.265156 +L 94.199463 469.388125 +L 91.735705 467.503495 +L 90.338351 464.833726 +L 85.469066 461.042093 +L 84.361795 458.967626 +L 87.02588 461.374035 +L 88.460209 461.452549 +L 91.585486 463.440854 +L 92.5817 463.176726 +L 94.787083 464.187848 +L 96.21491 462.310481 +L 95.859661 459.92194 +L 96.260136 457.81039 +L 95.384269 454.073268 +L 96.191149 452.353649 +L 96.941826 449.331787 +L 96.628085 442.278812 +L 97.620757 440.146148 +L 97.403511 438.13015 +L 96.185261 433.841039 +L 94.750447 431.398908 +L 93.939876 428.184053 +L 94.918674 424.070685 +L 97.371636 420.884327 +L 100.523404 419.905405 +L 102.185409 418.118937 +L 106.900812 416.836534 +L 110.609118 417.143441 +L 110.64276 414.51204 +L 112.928835 412.77324 +L 112.289979 408.128999 +L 114.470165 402.417045 +L 113.727248 396.277883 +L 114.504003 394.524713 +L 114.26724 391.472255 +L 113.930642 384.291693 +L 112.158391 375.361986 +L 112.684207 371.911509 +L 113.627772 371.883941 +L 115.901105 367.908057 +L 117.700346 362.608079 +L 122.570176 356.202158 +L 124.319633 353.141137 +L 125.267693 345.130331 +L 124.374932 342.109231 +L 122.595651 335.732982 +L 121.032611 334.079062 +L 117.55972 333.660892 +L 114.397163 332.029077 +L 109.08662 326.121165 +L 103.190402 321.590782 +L 97.512995 321.358699 +L 90.219731 318.14638 +L 86.066722 319.330151 +L 86.572013 316.618144 +L 84.761213 313.540957 +L 78.888033 309.966673 +L 74.573118 307.764296 +L 72.068077 310.798636 +L 71.8996 305.771605 +L 66.12804 304.396451 +L 65.135773 302.781602 +L 67.584825 298.910584 +L 67.562309 295.441792 +L 65.864185 294.434601 +L 64.413981 285.47342 +L 63.784895 282.616258 +L 62.707824 282.736341 +L 62.317038 280.625963 +L 59.370824 276.030234 +L 57.25841 274.587875 +L 56.299274 273.917272 +L 53.29 272.189932 +L 50.965623 272.261266 +L 50.587646 273.124812 +L 47.371258 271.687018 +L 46.464485 269.799829 +L 45.275992 267.162828 +L 44.299013 266.904432 +L 44.498198 264.321211 +L 43.275132 260.837998 +L 41.840187 258.758198 +L 41.016321 257.435886 +L 39.72266 257.352133 +L 39.884571 253.303621 +L 38.437342 250.571185 +L 36.634844 249.892877 +L 36.228292 247.419096 +L 38.419561 246.277713 +L 35.673218 245.88953 +L 32.911809 245.710066 +L 32.679209 246.953913 +L 31.237322 248.285489 +L 29.753686 247.372368 +L 28.971627 244.927532 +L 26.801549 245.013948 +L 25.128632 244.612513 +L 25.344183 242.958327 +L 24.678249 239.940812 +L 23.468427 239.579077 +L 23.535897 235.96927 +L 22.639544 237.382403 +L 22.421628 239.764824 +L 20.026075 241.267113 +L 19.442835 245.034235 +L 19.645912 246.904237 +L 18.718782 250.398917 +L 17.990237 250.544947 +L 18.030352 246.452983 +L 19.19921 243.757257 +L 19.681782 241.17548 +L 19.599643 238.801121 +L 20.620797 238.463862 +L 20.892806 237.310318 +L 21.531809 235.729353 +L 21.38342 234.328112 +L 20.896448 233.883074 +L 19.674862 236.056993 +L 18.876494 236.999502 +L 17.261448 239.167465 +L 16.264237 238.433158 +L 15.96189 239.428643 +L 15.118201 239.261132 +L 13.970163 241.314522 +L 13.014872 245.418208 +L 12.733744 247.053314 +L 12.151086 247.408183 +L 10.745986 250.557205 +L 10.19602 250.181197 +L 10.000626 248.650277 +L 9.894549 247.02064 +L 9.584064 245.975996 +L 9.07385 245.570054 +L 9.188963 245.099485 +L 8.729069 244.574113 +L 8.184624 245.909729 +L 7.512821 246.649808 +L 6.977072 247.720092 +L 6.547999 247.867688 +L 6.537297 246.531619 +L 6.406776 246.894659 +L 6.166323 246.500457 +L 6.346986 245.460064 +L 6.401928 243.530364 +L 6.471221 241.219072 +L 6.600544 239.066645 +L 6.961908 236.279318 +L 7.036147 235.405294 +L 7.274676 234.055859 +L 7.639147 232.885529 +L 7.870834 231.559234 +L 8.369744 229.50019 +L 8.55909 229.08377 +L 9.037251 226.86853 +L 9.297138 225.585274 +L 9.823023 223.447232 +L 10.360926 221.530696 +L 10.907413 219.969583 +L 11.306081 218.147627 +L 11.619556 217.085464 +L 11.792682 216.655832 +L 12.001568 215.212494 +L 12.007414 214.318088 +L 12.159347 213.112141 +L 12.263912 212.081695 +L 12.219987 211.907823 +L 12.208577 211.047251 +L 12.007067 211.381758 +L 11.87132 211.385892 +L 11.875301 210.957323 +L 11.835202 210.573582 +L 11.641916 211.064455 +L 11.389577 211.462787 +L 11.224053 211.394577 +L 11.235107 210.763836 +L 11.13371 210.937808 +L 11.226543 210.505237 +L 11.152642 210.454176 +L 10.875742 211.209563 +L 10.912767 210.977623 +L 10.963487 210.69277 +L 11.639251 208.170784 +L 12.015016 207.056372 +L 12.764521 204.681119 +L 12.89654 204.196538 +L 13.446771 202.528439 +L 13.668635 201.752387 +L 14.359239 199.756991 +L 14.788895 198.469005 +L 14.76776 198.368464 +L 15.000463 197.668691 +L 15.058377 197.67674 +L 14.694315 199.003692 +L 15.99512 195.401609 +L 16.742065 193.416825 +L 16.996992 192.518654 +L 18.089479 189.745908 +L 19.264423 187.174399 +L 20.142933 184.934134 +L 20.471848 183.846334 +L 20.185272 183.994134 +L 20.112015 183.471038 +L 20.011979 183.590734 +L 24.898908 170.859847 +L 36.406857 148.274226 +L 36.394774 148.331301 +L 36.86094 147.645961 +L 36.51065 148.432144 +L 36.843009 147.930683 +L 37.319515 146.986841 +L 38.329998 145.227885 +L 39.03246 143.932161 +L 39.405869 143.487838 +L 39.649871 143.478555 +L 39.50592 144.292022 +L 40.083963 144.174438 +L 39.644284 145.484605 +L 38.642716 147.701387 +L 38.584693 147.986895 +L 40.128937 146.388534 +L 39.925889 147.20442 +L 38.264732 151.166088 +L 37.097115 153.700072 +L 35.417325 156.567798 +L 34.594868 158.370061 +L 33.130079 161.979674 +L 31.566949 166.069623 +L 31.323684 167.261874 +L 30.496969 169.320626 +L 30.594522 170.066399 +L 31.084267 169.607342 +L 32.578647 166.92566 +L 33.456687 165.180018 +L 34.973351 162.148159 +L 37.267444 156.773215 +L 38.277424 154.85088 +L 39.65674 151.552518 +L 41.531294 147.674325 +L 43.162741 144.58488 +L 45.084784 141.571655 +L 46.945093 139.199308 +L 48.597244 137.371933 +L 51.163204 135.010513 +L 52.186457 133.642615 +L 53.601701 132.276016 +L 54.200209 132.184059 +L 56.401226 129.957063 +L 58.197183 129.082576 +L 60.913015 126.220381 +L 63.642406 122.057918 +L 64.549229 120.612019 +L 64.475513 120.233059 +L 67.260018 116.375181 +L 67.607028 114.893615 +L 67.730855 115.703393 +L 69.459187 113.102112 +L 70.963562 111.453909 +L 68.533833 114.791976 +L 67.872409 116.493316 +L 65.548529 119.132937 +L 65.519282 119.324936 +L 68.448049 116.293699 +L 69.939478 114.844528 +L 71.055128 113.33282 +L 71.363949 112.545304 +L 72.816487 110.351073 +L 72.367275 111.443853 +L 72.427583 111.721312 +L 71.716924 112.763179 +L 74.834255 109.962074 +L 77.297448 107.223212 +L 77.132989 106.950613 +L 78.323348 105.972763 +L 77.90604 106.438289 +L 78.595986 106.633743 +L 81.125352 105.913902 +L 81.374588 105.088523 +L 79.149493 105.351999 +L 80.97367 104.498481 +L 81.787454 104.491045 +L 82.45096 104.43453 +L 83.886006 103.994047 +L 84.417326 104.217631 +L 85.781592 103.803111 +L 86.783801 102.731751 +L 87.104027 101.791408 +L 86.083657 103.218931 +L 85.638008 102.995441 +L 86.914406 100.872677 +L 88.630571 98.869766 +L 89.314872 98.287327 +L 92.144514 96.012482 +L 94.445822 95.224656 +L 96.926127 94.246441 +L 99.968052 92.769391 +L 100.800941 91.519217 +L 102.763963 91.389349 +L 105.242722 91.717352 +L 99.993654 94.269102 +L 97.344735 97.430903 +L 98.24863 97.926054 +L 102.242964 95.553693 +L 105.094885 94.33036 +L 110.643248 92.743176 +L 114.836778 90.676122 +L 114.853314 89.191316 +L 117.041523 86.566459 +L 111.777611 90.352618 +L 108.628078 90.432276 +L 108.247312 88.270394 +L 110.255356 85.48332 +L 113.420705 81.606961 +L 117.051611 79.52855 +L 117.480269 77.617633 +L 115.119246 77.567019 +L 109.560056 79.863086 +L 103.082413 84.00189 +L 101.548109 84.341731 +L 106.070319 81.429276 +L 112.428931 77.267964 +L 115.566559 76.103155 +L 119.168916 73.910536 +L 120.773649 73.943569 +L 122.814631 74.347109 +L 125.23898 75.588232 +L 128.435683 75.45509 +L 133.224711 73.006717 +L 137.156983 72.211209 +L 139.541683 71.216651 +L 142.032287 70.146383 +L 145.620357 66.500423 +L 146.208818 65.215252 +L 145.380458 64.627527 +L 147.646167 61.900471 +L 147.757375 60.78138 +L 146.289863 59.64989 +L 147.060673 57.732469 +L 147.317928 55.771775 +L 150.166139 54.01885 +L 153.255482 50.359496 +L 156.61755 46.57169 +L 159.258327 44.026964 +L 156.72045 45.13149 +L 151.363576 47.926887 +L 147.684075 49.156115 +L 149.312393 47.379207 +L 149.082221 46.760965 +L 153.916382 43.320375 +L 157.392271 41.151872 +L 156.258279 40.603456 +L 157.650573 39.563025 +L 159.20821 37.88097 +L 160.029453 36.872181 +L 158.378583 37.349067 +L 158.181679 36.909278 +L 158.432115 35.915824 +L 157.083809 36.331645 +L 150.50174 40.252534 +L 146.876324 42.66446 +L 141.812809 45.166179 +L 139.331143 47.470445 +L 136.230326 49.97391 +L 133.545718 51.879166 +L 130.358953 53.747116 +L 126.832947 55.515434 +L 124.060082 56.501144 +L 122.322257 58.246032 +L 116.375503 63.207883 +L 112.054048 66.299794 +L 110.358559 67.130123 +L 113.300102 63.730861 +L 117.414011 60.089983 +L 121.329098 56.988247 +L 124.914048 54.483049 +L 124.919245 53.932911 +L 124.677824 53.355952 +L 126.183203 51.887887 +L 127.122615 50.801221 +L 129.014959 49.304913 +L 130.539797 48.014977 +L 132.363747 46.393489 +L 131.688042 46.580163 +L 131.688041 46.580163 +L 137.154941 43.029917 +L 138.964732 41.906858 +L 139.3042 41.634172 +L 139.304201 41.634172 +L 141.153679 40.433106 +L 144.947137 38.246069 +L 148.311855 36.408976 +L 153.105588 33.969664 +L 156.586207 32.353377 +L 157.0836 32.30094 +L 159.999784 30.854093 +L 161.89581 30.074392 +L 162.239951 30.202839 +L 165.397732 28.950261 +L 167.418284 28.06989 +L 171.392324 26.481592 +L 173.740054 25.509494 +L 172.530652 26.303332 +L 173.528802 26.229913 +L 177.245108 25.146475 +L 179.512801 24.200236 +L 181.783339 23.043591 +L 184.449789 22.147621 +L 186.734255 21.213101 +L 188.713874 20.106045 +L 189.105495 19.620954 +L 189.242835 19.272284 +L 184.088817 21.238511 +L 179.971408 22.739857 +L 176.50786 23.963261 +L 179.569569 22.500095 +L 183.04789 21.162315 +L 186.013759 19.809512 +L 182.335032 21.013819 +L 187.132945 19.182365 +L 188.17003 18.547304 +L 190.435608 17.843986 +L 195.868058 15.805049 +L 198.063667 14.975955 +L 198.991197 14.551407 +L 195.739182 15.507324 +L 192.32115 16.660686 +L 199.003106 14.095723 +L 200.670099 13.649054 +L 199.127055 14.175271 +L 197.14719 14.848473 +L 196.515742 15.110744 +L 198.000978 14.741972 +L 202.245245 13.424947 +L 204.581452 12.614685 +L 206.690061 12.065467 +L 207.664458 11.774921 +L 217.132761 9.237897 +L 217.715431 9.100303 +L 220.200465 8.504729 +L 222.569532 7.912938 +L 221.947211 8.048812 +L 222.489773 7.845104 +L 222.075261 7.913558 +L 222.075261 7.913558 +L 228.121433 6.293491 +L 231.815044 5.708481 +L 231.289444 5.87382 +L 229.373956 6.321549 +L 228.523746 6.575925 +L 230.614615 6.141755 +L 233.057644 5.641064 +L 234.730193 5.246766 +L 234.730194 5.246766 +L 257.895803 1.577694 +L 288 0 +L 318.104197 1.577694 +L 340.122404 5.065035 +L 340.063038 5.066866 +L 340.762066 5.22718 +L 347.451704 6.583832 +L 344.492903 5.757255 +L 344.492901 5.757254 +L 347.878567 6.293491 +L 350.066061 6.879628 +L 350.060307 6.884019 +L 348.494931 6.512738 +L 347.448787 6.355432 +L 349.994888 6.97857 +L 352.252415 7.510761 +L 353.403271 7.773831 +L 354.512842 8.07114 +L 354.675728 8.17017 +L 357.610576 9.007681 +L 356.63027 8.832641 +L 357.33286 9.216739 +L 358.688574 9.580034 +L 359.427535 10.028671 +L 361.80971 10.872688 +L 361.884336 11.037414 +L 364.973477 12.049683 +L 367.566719 12.67962 +L 367.972507 13.128904 +L 367.376694 13.356694 +L 367.972743 13.862953 +L 373.051758 15.473213 +L 377.200848 16.686406 +L 374.043383 16.013487 +L 373.665154 16.36941 +L 374.901237 17.232793 +L 378.413263 18.388197 +L 377.146211 18.221438 +L 372.840889 16.561156 +L 370.067551 15.737953 +L 369.980856 15.836393 +L 373.36133 16.956888 +L 375.679897 18.325158 +L 379.993653 19.568477 +L 384.181106 21.505864 +L 386.811869 22.516378 +L 387.71263 22.507508 +L 390.468977 23.853441 +L 393.314483 24.886598 +L 395.005798 25.989173 +L 397.064775 27.054296 +L 397.573162 27.776239 +L 398.915633 28.6091 +L 397.550409 28.485046 +L 392.461187 25.216129 +L 389.707344 23.823632 +L 386.375602 22.804585 +L 380.326557 20.224257 +L 377.000905 19.027533 +L 375.248339 18.557165 +L 372.010138 17.063803 +L 369.370663 16.183283 +L 366.914552 16.082363 +L 367.416946 16.489462 +L 371.119235 18.084559 +L 373.987205 19.905096 +L 375.403557 20.478973 +L 379.156423 21.847887 +L 380.990805 22.793656 +L 382.23213 22.810401 +L 383.177353 23.215048 +L 386.155343 24.097864 +L 387.988684 25.242394 +L 380.456566 23.506708 +L 378.066827 23.092153 +L 374.391122 22.904605 +L 375.292523 23.614229 +L 378.581938 24.618006 +L 380.253931 26.118343 +L 376.762789 25.048373 +L 376.973931 26.093713 +L 375.165459 26.401805 +L 375.710289 27.210047 +L 373.956004 27.15488 +L 372.727733 25.767651 +L 371.738219 25.763545 +L 370.926398 28.002734 +L 370.00526 29.29355 +L 371.88452 30.734581 +L 370.534393 31.417648 +L 368.31541 30.745007 +L 366.464031 29.512544 +L 367.727057 29.090521 +L 365.09159 27.949857 +L 360.367 27.584142 +L 363.360065 28.834402 +L 364.61581 30.204102 +L 367.709849 31.441527 +L 369.07922 33.08735 +L 366.600495 32.393799 +L 365.05526 32.370744 +L 364.399472 34.934811 +L 367.638981 36.619864 +L 366.962011 37.317854 +L 361.15374 36.078586 +L 361.024372 37.036476 +L 363.315901 38.065612 +L 364.411036 39.293626 +L 362.3284 38.730979 +L 359.239512 38.112422 +L 355.140968 34.516222 +L 350.522169 32.957336 +L 351.414358 32.592381 +L 360.736022 33.901658 +L 362.534841 33.095355 +L 362.647629 31.761171 +L 360.524036 30.270514 +L 357.97332 29.304436 +L 349.235388 27.244401 +L 344.591763 26.986528 +L 340.628221 25.860025 +L 339.958208 26.667077 +L 336.819298 25.446097 +L 338.049289 24.802921 +L 331.842443 23.559853 +L 329.676076 24.073286 +L 326.892789 24.096492 +L 326.152004 25.875842 +L 323.466974 25.857499 +L 320.651531 26.888832 +L 317.781794 29.739656 +L 315.713603 31.507998 +L 313.189236 36.163039 +L 310.665591 39.701831 +L 307.143865 42.433029 +L 301.647027 44.730646 +L 299.776893 46.494784 +L 301.457597 53.002213 +L 302.819323 56.159934 +L 306.687436 57.651842 +L 310.052704 56.905536 +L 314.301676 53.412505 +L 316.491018 55.187036 +L 319.663313 59.404982 +L 322.920448 62.874709 +L 324.665056 65.849688 +L 327.834952 65.635277 +L 328.564672 63.090507 +L 331.947507 63.315914 +L 332.363605 60.364752 +L 331.294249 55.272916 +L 333.576289 54.512504 +L 334.262622 51.17888 +L 331.24443 49.669438 +L 328.659269 47.746278 +L 328.417358 43.876127 +L 331.318437 41.491358 +L 333.322051 39.323486 +L 331.998906 37.798437 +L 332.707365 36.018951 +L 335.452537 35.214756 +L 338.962098 36.253085 +L 339.986272 37.281398 +L 339.105882 37.851662 +L 336.511789 40.767287 +L 335.684382 42.464875 +L 335.613776 44.039628 +L 338.132563 46.439066 +L 339.216765 49.183576 +L 341.97267 50.030689 +L 344.223723 51.53411 +L 347.605408 50.788953 +L 350.884151 49.587997 +L 354.726232 49.185613 +L 358.012327 50.416167 +L 356.630941 52.113523 +L 354.351727 52.304687 +L 351.556515 51.936632 +L 348.920317 52.474139 +L 346.447036 53.396453 +L 347.635489 55.072849 +L 349.776352 56.066569 +L 350.44034 55.657708 +L 351.197976 57.430284 +L 352.057261 59.778293 +L 350.080298 59.914701 +L 346.866734 57.716872 +L 345.056847 58.833924 +L 344.771467 60.799993 +L 345.81493 63.144841 +L 347.636588 65.786799 +L 344.38408 66.948692 +L 344.370922 68.391054 +L 341.690732 68.430736 +L 341.162945 67.646025 +L 338.196705 67.178774 +L 335.102462 68.367219 +L 331.198459 69.985205 +L 329.537121 70.996163 +L 327.869739 69.976859 +L 324.277099 68.750374 +L 322.904058 69.676268 +L 320.118701 70.344633 +L 319.874468 69.180163 +L 316.799942 68.467319 +L 316.475376 67.216206 +L 315.365778 65.66422 +L 316.847061 63.351382 +L 317.749924 63.680112 +L 318.124774 62.48517 +L 316.530544 62.036828 +L 315.994537 61.172402 +L 316.542292 60.154572 +L 316.23311 58.581845 +L 314.311694 59.475439 +L 313.566159 60.33533 +L 311.233853 60.559051 +L 310.640931 61.49707 +L 310.346695 62.343442 +L 311.031603 65.570831 +L 312.514541 67.337673 +L 312.991914 69.178528 +L 313.887144 70.399024 +L 312.186204 72.056064 +L 311.514413 71.33147 +L 309.078837 71.539993 +L 308.604974 72.249212 +L 306.123213 72.179826 +L 302.190093 73.612441 +L 299.944967 78.63886 +L 298.402693 79.597857 +L 295.923005 80.295091 +L 293.190274 81.007739 +L 292.313478 83.894785 +L 284.76026 86.681233 +L 281.725279 85.137964 +L 282.631904 89.219168 +L 277.115933 88.264929 +L 272.776297 89.028529 +L 272.895133 91.696084 +L 277.955185 93.145 +L 280.381114 95.034689 +L 283.832997 98.992836 +L 282.997308 106.659967 +L 281.059952 109.007368 +L 275.173241 108.853313 +L 272.1372 109.041369 +L 268.321249 108.340877 +L 263.456557 108.320888 +L 259.123639 107.562712 +L 253.639068 110.336065 +L 254.889522 112.078012 +L 254.327365 114.923064 +L 254.313797 116.282705 +L 254.87489 117.745538 +L 254.744635 119.468621 +L 253.653762 121.918046 +L 253.179246 123.577984 +L 251.468038 125.057603 +L 250.819897 127.776455 +L 251.552851 129.376047 +L 253.250311 129.787368 +L 253.327629 132.383393 +L 252.358656 135.699278 +L 254.458489 135.257523 +L 256.496344 135.883028 +L 258.20061 134.795445 +L 261.862015 135.495569 +L 262.806424 137.966811 +L 264.194396 139.432402 +L 266.150957 139.806985 +L 267.796172 138.194089 +L 270.40446 136.698606 +L 274.234785 136.802799 +L 279.348582 136.759426 +L 282.260511 133.491119 +L 285.279914 132.652079 +L 286.157226 129.910785 +L 288.436495 128.042097 +L 286.916131 125.660626 +L 288.41007 122.297765 +L 290.749631 120.0256 +L 291.073888 118.654448 +L 295.906911 117.785539 +L 299.367886 115.082758 +L 299.06617 112.757175 +L 299.378679 110.362702 +L 304.625188 109.051105 +L 311.900912 110.05875 +L 314.94734 107.799606 +L 316.409525 107.494628 +L 318.248357 105.65749 +L 319.813781 105.111307 +L 322.893855 106.35397 +L 324.736616 106.780437 +L 326.471114 110.650652 +L 329.30896 112.898884 +L 333.096478 115.447181 +L 336.294744 117.213089 +L 339.067215 117.419522 +L 340.977989 119.01613 +L 343.49817 119.703735 +L 344.950763 121.442958 +L 346.597152 121.918477 +L 348.167787 123.96674 +L 350.134766 126.332541 +L 349.503088 127.242146 +L 349.18715 129.503617 +L 349.428056 130.789359 +L 350.952572 130.425793 +L 352.215442 126.784591 +L 353.72636 126.496908 +L 353.680696 124.321234 +L 350.657729 122.862232 +L 351.607272 120.16992 +L 354.943827 120.756337 +L 357.441651 122.613329 +L 357.759444 121.124557 +L 357.190657 120.371414 +L 353.552627 118.336087 +L 350.597965 117.181357 +L 347.017109 115.805561 +L 347.846609 114.978934 +L 346.765192 114.114469 +L 343.948655 114.20778 +L 339.261252 111.085764 +L 336.793434 107.864829 +L 333.08569 105.963296 +L 331.55007 104.010326 +L 331.76193 102.902785 +L 331.19216 101.00233 +L 333.702547 99.59516 +L 336.543479 100.091877 +L 335.860453 100.454277 +L 335.752363 100.51855 +L 335.968479 101.847354 +L 337.271763 103.1129 +L 337.953717 101.431704 +L 340.300912 101.986272 +L 340.673208 103.282937 +L 342.637938 104.872133 +L 342.006401 105.178201 +L 345.630717 107.978573 +L 349.12696 109.063885 +L 351.527317 110.429414 +L 355.221723 111.802775 +L 356.956778 112.548232 +L 358.303805 113.821624 +L 359.12897 114.108856 +L 359.899178 114.722242 +L 359.760782 115.985994 +L 360.205179 118.755925 +L 361.034571 120.68836 +L 363.406618 122.001613 +L 363.721027 122.903494 +L 364.415939 123.172088 +L 364.976319 124.339579 +L 367.481633 126.64323 +L 369.428171 128.51353 +L 370.818882 131.284569 +L 373.106413 134.622951 +L 376.662028 136.368031 +L 379.123991 136.224311 +L 376.681422 132.526543 +L 378.889683 131.996294 +L 377.192696 129.88965 +L 380.886855 130.875506 +L 380.121399 128.507278 +L 377.971368 127.365161 +L 375.392685 125.524391 +L 376.471947 124.566091 +L 374.09708 122.69766 +L 372.558082 120.279342 +L 372.945479 119.360049 +L 375.468519 121.393682 +L 377.433452 121.314738 +L 379.001401 120.57814 +L 375.832282 118.38398 +L 379.678357 117.165435 +L 381.604847 117.47453 +L 383.731981 117.501647 +L 383.982661 118.340031 +L 385.733652 120.188036 +L 387.793552 117.872958 +L 388.765672 116.557959 +L 392.647702 116.150758 +L 392.858649 115.136258 +L 389.456147 113.978874 +L 388.457264 112.462698 +L 386.495017 110.250136 +L 386.538791 107.383989 +L 387.525419 105.691166 +L 386.374081 101.002725 +L 387.481692 101.314368 +L 388.610195 100.419593 +L 388.084523 99.4435 +L 389.112341 96.537493 +L 389.204539 94.425405 +L 391.705355 93.824045 +L 392.622461 95.198512 +L 397.675427 95.888613 +L 398.975134 96.697308 +L 396.812832 98.107387 +L 396.656871 98.861046 +L 400.4716 99.786314 +L 400.732938 101.615242 +L 402.790605 102.297806 +L 405.62895 99.849886 +L 408.423392 98.995329 +L 408.222251 97.613233 +L 405.438919 98.023567 +L 403.532893 97.202542 +L 402.076246 94.887179 +L 403.721652 93.355032 +L 406.209765 92.993273 +L 407.31783 91.682768 +L 409.297174 91.243401 +L 411.319869 90.487355 +L 411.898902 91.282002 +L 408.857067 93.055937 +L 411.271637 94.417017 +L 410.824388 97.698716 +L 409.115168 98.436067 +L 412.8238 100.501165 +L 416.863835 101.720004 +L 422.303266 104.714151 +L 424.008915 105.826109 +L 425.81055 106.15772 +L 428.243664 107.471732 +L 430.469646 110.094206 +L 431.001573 111.816393 +L 428.768533 114.149751 +L 426.07805 113.975694 +L 422.96112 114.834548 +L 417.879397 113.576691 +L 411.195962 111.111146 +L 406.135846 111.498745 +L 402.99137 112.829078 +L 400.272175 115.632596 +L 393.817948 115.41661 +L 393.633833 118.553691 +L 388.49566 118.957536 +L 386.065792 123.037185 +L 388.951989 124.913103 +L 388.33347 128.221562 +L 391.6911 130.432225 +L 395.186153 134.540413 +L 399.039883 134.282752 +L 403.230161 136.383489 +L 405.479769 135.754388 +L 405.653697 133.949784 +L 409.425482 133.896374 +L 413.051318 136.026999 +L 418.017399 135.25297 +L 419.338894 132.676688 +L 422.494396 133.480331 +L 424.33905 132.994179 +L 423.758863 134.664378 +L 425.756698 136.519031 +L 425.655931 138.32981 +L 427.263216 141.603572 +L 427.258695 141.757913 +L 426.745571 144.92023 +L 426.833173 148.557863 +L 426.754216 148.608446 +L 426.657615 149.751811 +L 427.114816 153.129003 +L 426.889098 155.261921 +L 427.214994 155.499786 +L 426.672035 157.031459 +L 425.282644 158.262352 +L 422.397159 158.169924 +L 419.161653 157.274363 +L 418.765818 158.783611 +L 417.089852 156.617105 +L 414.3138 156.191633 +L 411.169069 156.726964 +L 410.010108 158.086295 +L 407.51954 159.64938 +L 405.575182 159.033158 +L 401.443333 157.882771 +L 397.451039 156.862608 +L 392.342051 157.152379 +L 391.028087 155.715636 +L 387.040955 155.345456 +L 385.616681 154.633096 +L 384.159139 154.66921 +L 382.355975 152.732643 +L 376.850701 152.013478 +L 374.271603 152.7097 +L 371.85281 154.885902 +L 371.038222 157.097434 +L 372.661579 160.50694 +L 371.115046 162.644604 +L 369.338499 163.880246 +L 364.561297 161.759327 +L 358.439926 160.028566 +L 354.594723 159.249643 +L 352.038893 155.31665 +L 346.289957 153.450192 +L 342.751656 152.781374 +L 341.074676 153.197564 +L 336.034461 151.761665 +L 334.381614 151.097894 +L 333.095025 149.018059 +L 330.96232 148.976934 +L 329.910939 146.597554 +L 332.328527 144.358509 +L 332.384975 140.595649 +L 330.862324 139.547444 +L 330.63693 137.559684 +L 332.340356 135.432355 +L 331.948492 134.619513 +L 328.802024 136.242733 +L 328.647265 134.085592 +L 325.826673 133.617953 +L 321.707878 135.393686 +L 319.01224 135.686572 +L 317.301697 134.71319 +L 313.051312 134.790614 +L 309.405817 136.505449 +L 307.343586 135.886256 +L 300.721849 136.273508 +L 293.91858 137.059488 +L 290.041302 138.370502 +L 287.481265 140.147453 +L 283.067687 140.89322 +L 279.085812 143.250884 +L 277.303836 143.198522 +L 273.095458 142.219964 +L 269.193171 142.495721 +L 266.84259 140.639546 +L 263.855074 140.59362 +L 262.38539 143.242242 +L 259.301317 147.736945 +L 256.08505 149.521041 +L 251.741365 151.48634 +L 248.771818 154.448584 +L 247.979451 156.791046 +L 245.997162 160.631436 +L 246.528261 166.284371 +L 242.573716 170.045347 +L 240.287296 171.23259 +L 236.552408 174.32176 +L 232.466497 174.755831 +L 230.000136 176.553693 +L 226.697581 181.245442 +L 223.5913 182.884496 +L 221.66917 185.746972 +L 221.271834 188.251821 +L 219.787513 190.978963 +L 218.212664 191.706656 +L 215.400769 194.662318 +L 213.485567 197.985132 +L 213.587002 199.610031 +L 211.846214 202.067742 +L 209.984254 203.331231 +L 209.522222 205.553853 +L 209.104289 207.58469 +L 211.252934 209.733863 +L 212.187465 212.065246 +L 211.495192 214.473587 +L 211.812112 216.905956 +L 211.875943 221.740978 +L 210.904407 226.33891 +L 209.41271 228.729064 +L 209.596077 231.391604 +L 208.293228 233.895476 +L 205.775022 237.304529 +L 203.662654 238.18799 +L 205.844862 240.015033 +L 207.494167 243.927066 +L 206.746887 246.115471 +L 207.271422 249.950538 +L 207.50547 251.02448 +L 208.87947 252.119336 +L 208.807864 252.877424 +L 209.812834 254.310819 +L 211.789473 254.690783 +L 214.218172 256.838877 +L 215.564128 257.689757 +L 216.20997 258.807455 +L 216.651024 261.029598 +L 217.803416 262.051253 +L 219.005049 262.726054 +L 220.795767 264.726397 +L 222.801767 267.73616 +L 223.268867 271.455554 +L 224.06029 273.305205 +L 226.514971 276.044897 +L 229.974771 278.13311 +L 231.28316 278.529422 +L 234.513251 281.827371 +L 238.65372 284.64282 +L 243.082726 288.533589 +L 248.161972 290.995578 +L 249.463421 290.967024 +L 250.421768 291.110728 +L 255.364234 289.320217 +L 258.834539 287.902066 +L 264.747378 287.071875 +L 267.948088 287.034701 +L 271.428613 288.037103 +L 273.70407 287.996551 +L 278.159419 289.440436 +L 282.674371 287.993237 +L 285.459473 286.272363 +L 293.299989 283.326754 +L 297.320619 282.245408 +L 301.444558 281.644939 +L 305.847818 281.627 +L 309.592856 281.542196 +L 313.148695 284.828052 +L 314.819975 288.452886 +L 317.51426 291.574431 +L 321.500154 291.645612 +L 323.403539 290.49963 +L 325.292175 290.743087 +L 330.42348 288.870807 +L 330.383502 290.260593 +L 331.660234 290.964728 +L 332.692035 293.203462 +L 334.960635 294.024 +L 336.927008 297.316586 +L 336.235816 301.292916 +L 334.561206 306.952961 +L 335.491704 307.695885 +L 334.498801 311.426125 +L 333.293008 315.087792 +L 332.20568 316.701971 +L 332.042616 318.36568 +L 335.031543 323.481282 +L 338.271672 327.544459 +L 343.282186 332.480513 +L 347.231957 337.660068 +L 348.465198 341.354206 +L 349.115887 342.872353 +L 348.629437 343.839315 +L 350.991766 346.908471 +L 351.893158 350.17752 +L 353.209339 354.879552 +L 351.652964 356.84613 +L 351.356942 357.864713 +L 352.431888 360.762444 +L 353.592156 363.682042 +L 354.952542 365.383804 +L 355.073666 368.117907 +L 354.39338 371.697565 +L 352.750326 373.866828 +L 349.843048 377.05501 +L 348.601344 379.03369 +L 346.821542 383.349324 +L 346.459385 385.386625 +L 344.571077 389.734213 +L 343.665306 393.867949 +L 343.921882 396.792363 +L 343.967162 400.347437 +L 347.42721 404.77704 +L 348.206305 407.634214 +L 350.146756 413.043171 +L 352.141914 416.720654 +L 353.713755 418.532082 +L 354.031862 420.956699 +L 353.54173 426.257918 +L 354.212472 432.961734 +L 354.884662 436.070663 +L 355.271933 440.222634 +L 356.505387 443.285228 +L 359.176583 446.389987 +L 361.27592 451.722564 +L 361.280604 451.733235 +L 362.722538 455.160212 +L 364.653069 458.89167 +L 364.118793 461.976609 +L 362.666115 462.741838 +L 363.399785 465.369118 +L 362.867127 467.68094 +L 363.155887 468.719103 +L 363.465553 468.170765 +L 364.758 469.865313 +L 366.065519 469.896825 +L 367.377888 471.231142 +L 369.166748 471.082631 +L 371.938439 469.535553 +L 375.406084 468.8059 +L 379.803801 467.117661 +L 381.339737 467.265009 +L 383.79754 466.698321 +L 387.703547 467.295151 +L 389.80135 466.470407 +L 391.913333 466.956865 +L 392.736778 465.842282 +L 394.715359 465.554597 +L 399.111132 463.8435 +L 402.507337 461.898792 +L 405.90997 459.363584 +L 411.459045 454.96468 +L 414.503969 451.913144 +L 416.208704 449.731984 +L 418.447179 447.532536 +L 419.363628 446.888867 +L 422.561121 444.63034 +L 424.106305 442.671109 +L 425.598726 439.112403 +L 427.43984 435.937447 +L 428.405052 433.646498 +L 427.515952 433.409111 +L 427.689327 431.598564 +L 429.795588 429.892552 +L 434.993118 427.257744 +L 438.496509 425.58355 +L 440.484812 423.916471 +L 441.534042 422.037076 +L 440.850779 421.330976 +L 441.995668 419.222624 +L 443.20501 414.815428 +L 442.479781 415.082244 +L 442.758523 413.735985 +L 442.630467 411.127638 +L 441.581654 407.792679 +L 442.612995 404.509185 +L 444.434853 403.349461 +L 447.813462 399.904861 +L 449.467532 398.96541 +L 454.792604 393.692551 +L 459.503462 391.132875 +L 463.27079 389.108195 +L 466.193436 386.017059 +L 468.238056 382.627747 +L 469.943991 379.151699 +L 469.6954 376.865793 +L 470.737504 369.385453 +L 470.881048 365.187406 +L 471.670321 360.347814 +L 471.328553 358.213367 +L 470.066024 357.250599 +L 469.017846 352.551802 +L 467.981748 349.578322 +L 468.452134 347.216471 +L 468.362845 345.738855 +L 469.672504 342.690379 +L 469.657814 341.417266 +L 467.311109 339.794842 +L 467.272849 337.010645 +L 469.429042 330.779001 +L 471.070959 329.027824 +L 471.973514 325.665226 +L 473.287654 323.567054 +L 473.948233 320.015444 +L 475.390874 319.543408 +L 476.385311 317.390417 +L 479.0741 315.194296 +L 479.943669 313.945162 +L 480.842393 311.241749 +L 484.914739 304.853165 +L 488.276602 300.760224 +L 493.512757 295.319759 +L 496.874633 290.944564 +L 500.576463 283.724504 +L 503.078603 277.832677 +L 505.303033 270.208444 +L 506.65861 263.615914 +L 507.537758 257.878667 +L 507.710864 252.398823 +L 508.112122 250.531469 +L 507.711778 247.921079 +L 507.54572 245.003189 +L 507.253196 243.640546 +L 506.080242 243.780594 +L 504.871949 245.633874 +L 503.27198 246.308105 +L 501.910976 247.195315 +L 500.895073 247.397244 +L 499.068211 247.755739 +L 498.028394 248.776128 +L 496.431802 249.260993 +L 493.6921 251.083707 +L 490.04462 252.010535 +L 486.923264 253.571674 +L 485.166901 253.704497 +L 483.290808 251.754571 +L 482.315307 249.755933 +L 481.023187 248.935008 +L 479.286611 247.700721 +L 481.171067 246.3459 +L 480.981212 244.276667 +L 479.903637 242.81099 +L 477.90844 241.463822 +L 476.512907 239.853103 +L 474.088121 237.155525 +L 471.593579 234.49882 +L 465.756853 230.222927 +L 463.289912 227.977766 +L 461.438468 223.552777 +L 458.176203 218.055363 +L 455.761946 216.397845 +L 454.08528 215.298488 +L 451.502171 209.607597 +L 449.873291 204.684672 +L 450.498118 203.759524 +L 448.205316 199.14174 +L 447.318417 198.207338 +L 441.930215 194.214618 +L 441.053976 191.143386 +L 441.591227 190.274089 +L 436.907827 185.270722 +L 435.016896 182.699284 +L 432.965621 180.246264 +L 428.180477 173.198963 +L 424.598451 168.7168 +L 421.673063 164.000886 +L 421.93053 163.569749 +L 426.463681 169.948138 +L 428.715843 171.898823 +L 430.369026 173.311668 +L 430.999281 172.457656 +L 431.302337 170.008748 +L 431.048781 166.513823 +L 431.496643 164.627257 +L 431.820985 165.27609 +L 431.934333 167.118627 +L 432.257444 168.72522 +L 432.437382 171.273028 +L 434.242671 171.139968 +L 437.022891 174.184638 +L 440.30988 177.773558 +L 442.70766 181.140149 +L 444.058231 182.112118 +L 445.737691 184.470425 +L 445.829375 185.542494 +L 447.750802 188.155508 +L 449.970154 188.997561 +L 452.154475 190.722165 +L 455.591863 195.831836 +L 456.149757 198.666096 +L 457.382069 201.958448 +L 460.865698 206.360924 +L 462.660029 207.033704 +L 465.906732 210.167543 +L 467.797054 214.034734 +L 470.577606 217.961785 +L 472.780957 219.563336 +L 473.455672 221.486573 +L 474.822905 222.872449 +L 475.697095 224.923262 +L 476.26777 227.04792 +L 476.016714 228.028454 +L 476.797936 230.242548 +L 476.128144 230.539682 +L 477.515784 232.479304 +L 478.844803 236.068475 +L 479.670398 237.477355 +L 480.007827 240.188986 +L 481.385591 243.004478 +L 483.873224 243.052461 +L 484.889817 242.284158 +L 486.630889 242.244086 +L 486.965229 240.938682 +L 487.810372 240.497915 +L 488.344538 239.124875 +L 489.159419 238.766858 +L 491.952037 238.25214 +L 493.910014 237.102384 +L 495.464186 234.871121 +L 496.495702 235.068025 +L 497.869643 234.655954 +L 500.058064 230.896602 +L 504.541166 228.084557 +L 507.09246 225.685895 +L 506.794034 224.00989 +L 506.874335 221.776718 +L 508.679063 220.225042 +L 509.941212 219.793932 +L 511.438647 217.92532 +L 513.091248 218.196268 +L 514.087475 216.68112 +L 513.574293 214.722197 +L 514.31831 213.367049 +L 515.993869 213.19171 +L 516.326608 212.09154 +L 515.93989 209.705012 +L 517.056037 207.686622 +L 518.237461 207.542308 +L 518.306099 206.917681 +L 517.051563 203.75176 +L 516.71959 201.269407 +L 516.88624 200.058581 +L 518.088844 200.150372 +L 517.957106 196.757398 +L 518.471212 195.091327 +L 518.405793 193.705703 +L 518.294697 190.761768 +L 517.929085 189.709173 +L 516.878084 189.238121 +L 515.683585 187.769654 +L 513.633701 185.237514 +L 511.8931 184.587301 +L 509.869427 184.224468 +L 507.853917 182.71715 +L 505.533768 179.687818 +L 503.778244 176.07448 +L 503.775038 175.194751 +L 503.249146 173.262862 +L 502.779918 172.904508 +L 502.678223 174.565313 +L 502.181044 177.617543 +L 501.358511 180.83542 +L 500.675183 184.2102 +L 498.986431 184.267273 +L 496.660332 184.408789 +L 494.71426 185.393679 +L 494.166132 184.121926 +L 493.739154 184.405178 +L 492.576659 182.679717 +L 492.217427 179.873553 +L 491.17485 177.164831 +L 489.782794 175.808638 +L 489.194548 176.38935 +L 489.312991 178.901015 +L 490.706293 182.265083 +L 489.873964 181.169514 +L 488.947648 179.684943 +L 487.64886 178.472107 +L 486.717663 176.954394 +L 486.440523 175.385771 +L 485.555715 173.503131 +L 482.853583 171.775398 +L 481.741392 170.217159 +L 479.903347 169.317361 +L 477.22264 165.504384 +L 474.910715 162.185012 +L 474.752314 161.131441 +L 473.324217 159.207509 +L 475.136872 159.259123 +L 475.466447 157.4062 +L 477.903361 158.706126 +L 479.100999 157.824414 +L 483.700047 163.602392 +L 487.315734 167.706997 +L 490.478046 168.711135 +L 494.604118 171.911875 +L 498.428813 173.04744 +L 500.11337 170.504107 +L 501.692657 169.438972 +L 503.207033 170.092321 +L 506.547141 175.582811 +L 509.499088 175.804035 +L 512.468196 176.496001 +L 517.236342 177.241599 +L 519.959583 176.099948 +L 523.195169 175.436734 +L 526.304598 173.906845 +L 529.18191 177.135909 +L 531.081087 180.356493 +L 532.829219 181.259488 +L 536.361909 184.7771 +L 537.55341 186.503022 +L 537.411142 188.381242 +L 541.613134 193.595682 +L 542.903018 193.890013 +L 543.996151 190.498012 +L 545.869251 194.847338 +L 547.835805 200.607669 +L 550.211315 206.615136 +L 553.504889 215.801782 +L 556.472541 222.109448 +L 557.4447 225.087628 +L 559.166459 231.112409 +L 560.827066 235.615814 +L 561.717907 237.801208 +L 563.099928 242.622183 +L 564.781631 249.366033 +L 566.506119 253.604825 +L 566.731084 252.006117 +L 566.57609 248.486478 +L 567.239628 246.693686 +L 566.687763 245.190868 +L 566.464126 241.224605 +L 566.883265 240.766078 +L 565.253296 232.393714 +L 564.591164 227.555835 +L 563.593368 223.523175 +L 561.808577 217.280217 +L 561.045562 213.459748 +L 561.3442 213.010316 +L 561.511855 210.905971 +L 561.501891 209.510819 +L 560.841293 207.296874 +L 560.476592 203.74388 +L 559.906495 200.418702 +L 558.510731 194.366729 +L 557.867986 190.597554 +L 556.97257 187.597879 +L 555.598044 184.101504 +L 555.455913 182.630794 +L 555.547571 182.443322 +L 555.547571 182.443322 +L 561.904277 199.003106 +L 569.706509 228.121433 +L 574.422306 257.895803 +L 576 288 +L 576 288 +L 574.422306 318.104197 +L 569.706509 347.878567 +L 561.904277 376.996894 +L 551.101092 405.140153 +L 537.415316 432 +L 520.996894 457.282153 +L 502.02571 480.709615 +L 480.709615 502.02571 +L 457.282153 520.996894 +L 432 537.415316 +L 405.140153 551.101092 +L 387.991878 557.683698 +L 388.933106 557.288269 +L 388.238444 557.441112 +L 387.863706 557.454663 +L 387.651475 557.393929 +L 387.373279 557.338272 +L 387.499299 557.105282 +L 385.052831 557.944921 +L 383.547189 558.395112 +L 382.937394 558.435007 +L 383.297724 558.08934 +L 383.770394 557.716402 +L 383.05999 557.687321 +L 383.52261 557.299837 +L 383.892914 556.844016 +L 385.456541 556.09021 +L 385.61499 555.713284 +L 384.906152 555.674218 +L 384.086402 555.696692 +L 382.685003 555.972334 +L 380.882265 556.396828 +L 379.145825 556.860587 +L 377.100096 557.457597 +L 375.591523 558.092897 +L 373.722683 558.600982 +L 372.596461 558.68975 +L 370.811231 559.230581 +L 368.435769 559.921349 +L 367.609754 559.844528 +L 365.851791 560.287179 +L 363.980652 560.758602 +L 362.230915 561.187125 +L 360.021488 561.621682 +L 358.259049 561.937821 +L 355.809525 562.548949 +L 354.039051 562.901893 +L 352.259624 563.563366 +L 350.17997 563.995696 +L 349.90233 563.704426 +L 349.922283 563.262486 +L 348.216855 563.472514 +L 347.608982 563.20974 +L 347.978277 562.725536 +L 346.819913 562.585549 +L 345.092121 563.12905 +L 342.863721 563.967104 +L 341.037467 564.415327 +L 339.282164 564.631954 +L 337.456905 564.945729 +L 335.503598 565.380754 +L 333.661428 565.611919 +L 331.867381 565.860291 +L 330.147714 565.958879 +L 328.424535 566.030222 +L 326.550643 566.151535 +L 324.538921 566.440366 +L 324.065023 566.098342 +L 323.904979 565.687672 +L 322.277582 565.667277 +L 320.656112 565.567827 +L 318.954214 565.5929 +L 316.962946 565.698521 +L 315.027186 565.899611 +L 313.20715 566.415435 +L 313.01691 565.94768 +L 310.896427 565.918344 +L 308.908911 566.309514 +L 307.776423 566.82219 +L 305.746081 567.098889 +L 305.122242 566.672348 +L 304.291408 566.085861 +L 302.434127 566.28838 +L 301.33862 565.972066 +L 300.091273 566.441844 +L 298.5257 566.731214 +L 296.591615 566.944487 +L 294.817766 567.24794 +L 292.946479 567.425785 +L 291.066927 567.59884 +L 289.398789 567.813542 +L 287.637955 568.2061 +L 286.933155 567.720374 +L 285.087104 567.646404 +L 283.084817 567.77974 +L 281.067888 567.977046 +L 279.138648 567.893318 +L 278.553705 567.443115 +L 276.748571 567.306609 +L 276.15819 567.77122 +L 276.324749 568.20832 +L 274.429162 568.139847 +L 273.412714 567.736572 +L 271.46713 567.637908 +L 270.567236 567.945362 +L 270.250982 568.476866 +L 269.460206 568.894943 +L 268.297097 569.208091 +L 266.98771 569.426083 +L 265.760453 569.598678 +L 265.245549 569.900389 +L 265.32393 570.301662 +L 267.045768 570.566295 +L 267.169365 570.928634 +L 266.364434 571.168739 +L 265.740496 571.437503 +L 264.530272 571.661791 +L 263.615049 571.821477 +L 262.616291 571.971027 +L 261.579842 572.082345 +L 260.221563 572.132275 +L 258.623644 572.096486 +L 258.056744 572.128308 +L 256.937059 572.174674 +L 255.935346 572.247449 +L 255.9783 572.503576 +L 255.541837 572.661469 +L 255.173185 572.812497 +L 254.299892 572.892235 +L 253.727363 572.980051 +L 253.968487 573.101609 +L 256.008538 573.565279 +L 257.291603 573.795422 +L 258.77335 573.884714 +L 259.959874 573.886455 +L 261.4208 573.945035 +L 262.331865 574.147176 +L 263.52393 574.34369 +L 264.898451 574.537722 +L 264.393551 574.59807 +L 263.305936 574.594542 +L 262.291991 574.574744 +L 261.706333 574.592848 +L 261.298135 574.600615 +L 261.298135 574.600615 +L 260.456438 574.556503 +L 259.735748 574.505457 +L 259.775571 574.52082 +L 258.274705 574.442163 +L 257.594847 574.374639 z -M 313.344896 12.313022 -L 311.512427 12.042681 -L 310.414701 11.526165 -L 308.830105 11.876589 -L 309.980801 12.314856 -L 309.752827 12.713788 -L 311.948299 12.995145 +M 313.344896 12.313022 +L 311.512427 12.042681 +L 310.414701 11.526165 +L 308.830105 11.876589 +L 309.980801 12.314856 +L 309.752827 12.713788 +L 311.948299 12.995145 z -M 304.125395 9.976601 -L 302.540637 9.555226 -L 301.362569 9.630939 -L 301.485164 10.082565 -L 300.258736 10.128768 -L 299.383456 9.682439 -L 297.377977 10.193226 -L 298.820336 11.243018 -L 301.615614 12.407869 -L 303.493031 12.799093 -L 302.969166 13.350777 -L 306.071285 14.231606 -L 307.344181 14.136797 -L 306.637795 12.86765 -L 307.241804 12.564741 -L 306.619041 11.495995 -L 308.259166 10.878826 +M 304.125395 9.976601 +L 302.540637 9.555226 +L 301.362569 9.630939 +L 301.485164 10.082565 +L 300.258736 10.128768 +L 299.383456 9.682439 +L 297.377977 10.193226 +L 298.820336 11.243018 +L 301.615614 12.407869 +L 303.493031 12.799093 +L 302.969166 13.350777 +L 306.071285 14.231606 +L 307.344181 14.136797 +L 306.637795 12.86765 +L 307.241804 12.564741 +L 306.619041 11.495995 +L 308.259166 10.878826 z -M 342.707211 5.643145 -L 338.293702 4.857438 -L 333.871621 4.27271 -L 331.574691 4.036714 -L 332.833815 4.348107 -L 336.309725 5.073121 -L 337.250583 5.136857 -L 340.759265 5.698473 -L 342.526742 5.908197 -L 343.642023 5.939164 +M 342.707211 5.643145 +L 338.293702 4.857438 +L 333.871621 4.27271 +L 331.574691 4.036714 +L 332.833815 4.348107 +L 336.309725 5.073121 +L 337.250583 5.136857 +L 340.759265 5.698473 +L 342.526742 5.908197 +L 343.642023 5.939164 z -M 308.622127 8.881841 -L 306.207938 8.652265 -L 305.998994 9.047051 -L 304.442527 8.788995 -L 302.457282 9.207561 -L 304.057149 9.766259 -L 305.286052 9.752459 -L 305.902034 10.105907 -L 308.721156 10.238561 -L 310.907035 9.98817 -L 310.892407 9.250815 +M 308.622127 8.881841 +L 306.207938 8.652265 +L 305.998994 9.047051 +L 304.442527 8.788995 +L 302.457282 9.207561 +L 304.057149 9.766259 +L 305.286052 9.752459 +L 305.902034 10.105907 +L 308.721156 10.238561 +L 310.907035 9.98817 +L 310.892407 9.250815 z -M 273.192434 5.448143 -L 269.675547 5.129423 -L 267.802815 5.112981 -L 266.063805 5.409836 -L 264.653358 5.247662 -L 261.077595 5.670909 -L 259.45944 6.075389 -L 258.710735 6.722246 -L 258.826495 6.299487 -L 258.451048 6.158783 -L 258.804016 5.693507 -L 255.526558 6.092974 -L 256.323031 5.743806 -L 255.103699 5.601057 -L 253.334751 5.586358 -L 251.383147 5.698127 -L 249.545077 6.142786 -L 248.56631 6.159142 -L 244.267939 6.620771 -L 242.161087 6.962763 -L 241.468201 7.527921 -L 239.685735 7.894553 -L 236.171541 8.147223 -L 232.726695 8.378353 -L 230.85541 8.811239 -L 229.676838 9.493368 -L 230.161683 10.193305 -L 229.083669 10.081034 -L 226.636209 10.236417 -L 224.406316 11.215622 -L 223.450665 11.75677 -L 224.919061 11.893242 -L 226.469353 12.090924 -L 227.341838 12.389993 -L 226.531774 13.482101 -L 224.786707 14.105161 -L 224.073326 14.821873 -L 220.690845 16.634966 -L 218.586634 17.875143 -L 217.640836 18.581344 -L 212.995864 20.111747 -L 212.777221 20.658853 -L 214.246326 20.514005 -L 213.787855 21.246599 -L 213.135446 22.643752 -L 212.06699 21.902384 -L 211.109397 21.821839 -L 208.675557 22.789631 -L 206.123559 24.125471 -L 206.15031 24.930674 -L 207.643808 24.754697 -L 208.78404 24.522542 -L 211.328753 23.953463 -L 208.238843 25.498133 -L 206.261973 26.30919 -L 203.191519 26.892817 -L 197.703292 29.256717 -L 197.166741 30.122278 -L 194.007374 31.72113 -L 192.363605 34.048964 -L 189.316404 36.207528 -L 187.692048 37.875086 -L 185.878804 41.275426 -L 183.607924 43.928606 -L 183.345459 45.547307 -L 186.650093 45.87449 -L 186.666707 48.336847 -L 189.390168 48.382199 -L 193.235173 45.784588 -L 196.502665 43.637598 -L 198.164553 41.539687 -L 203.321398 39.710041 -L 206.109559 38.128165 -L 208.20482 36.408016 -L 211.408731 35.020635 -L 214.4013 34.628683 -L 217.25877 34.193456 -L 218.506327 34.169838 -L 223.912653 32.749829 -L 228.87139 30.476685 -L 231.48184 29.710413 -L 233.258632 29.79434 -L 238.792774 29.263361 -L 244.847633 27.742302 -L 250.777169 26.065532 -L 248.713807 25.871907 -L 244.734884 25.61857 -L 247.574009 24.617633 -L 248.453491 23.241387 -L 249.12343 24.460806 -L 249.555027 25.290971 -L 252.657644 24.996516 -L 253.514691 23.358427 -L 252.745976 22.097268 -L 251.342186 21.545581 -L 252.583982 21.0682 -L 254.564014 21.957398 -L 255.354581 21.110899 -L 254.924715 19.79349 -L 256.78417 19.856315 -L 258.942613 19.638827 -L 259.975627 19.016702 -L 259.182353 18.246031 -L 262.141292 18.212538 -L 261.95767 16.685007 -L 263.401259 16.571049 -L 264.522071 15.170066 -L 263.394344 14.266031 -L 265.711093 13.836424 -L 267.449772 13.824024 -L 267.243235 12.804416 -L 269.057293 11.214347 -L 270.839335 10.357462 -L 272.964405 9.438911 -L 271.157806 9.322092 -L 274.005891 9.179782 -L 274.781706 8.905392 -L 278.778698 8.11518 -L 278.830947 7.610473 -L 276.989402 7.34505 -L 273.048944 7.688713 -L 270.571839 8.055948 -L 272.441937 7.417394 -L 272.581431 7.001414 -L 270.713758 7.289576 -L 269.577672 6.832157 -L 267.174732 6.844154 -L 267.345208 6.639683 -L 270.766146 6.691915 -L 273.193624 6.738355 -L 275.025128 6.371501 +M 273.192434 5.448143 +L 269.675547 5.129423 +L 267.802815 5.112981 +L 266.063805 5.409836 +L 264.653358 5.247662 +L 261.077595 5.670909 +L 259.45944 6.075389 +L 258.710735 6.722246 +L 258.826495 6.299487 +L 258.451048 6.158783 +L 258.804016 5.693507 +L 255.526558 6.092974 +L 256.323031 5.743806 +L 255.103699 5.601057 +L 253.334751 5.586358 +L 251.383147 5.698127 +L 249.545077 6.142786 +L 248.56631 6.159142 +L 244.267939 6.620771 +L 242.161087 6.962763 +L 241.468201 7.527921 +L 239.685735 7.894553 +L 236.171541 8.147223 +L 232.726695 8.378353 +L 230.85541 8.811239 +L 229.676838 9.493368 +L 230.161683 10.193305 +L 229.083669 10.081034 +L 226.636209 10.236417 +L 224.406316 11.215622 +L 223.450665 11.75677 +L 224.919061 11.893242 +L 226.469353 12.090924 +L 227.341838 12.389993 +L 226.531774 13.482101 +L 224.786707 14.105161 +L 224.073326 14.821873 +L 220.690845 16.634966 +L 218.586634 17.875143 +L 217.640836 18.581344 +L 212.995864 20.111747 +L 212.777221 20.658853 +L 214.246326 20.514005 +L 213.787855 21.246599 +L 213.135446 22.643752 +L 212.06699 21.902384 +L 211.109397 21.821839 +L 208.675557 22.789631 +L 206.123559 24.125471 +L 206.15031 24.930674 +L 207.643808 24.754697 +L 208.78404 24.522542 +L 211.328753 23.953463 +L 208.238843 25.498133 +L 206.261973 26.30919 +L 203.191519 26.892817 +L 197.703292 29.256717 +L 197.166741 30.122278 +L 194.007374 31.72113 +L 192.363605 34.048964 +L 189.316404 36.207528 +L 187.692048 37.875086 +L 185.878804 41.275426 +L 183.607924 43.928606 +L 183.345459 45.547307 +L 186.650093 45.87449 +L 186.666707 48.336847 +L 189.390168 48.382199 +L 193.235173 45.784588 +L 196.502665 43.637598 +L 198.164553 41.539687 +L 203.321398 39.710041 +L 206.109559 38.128165 +L 208.20482 36.408016 +L 211.408731 35.020635 +L 214.4013 34.628683 +L 217.25877 34.193456 +L 218.506327 34.169838 +L 223.912653 32.749829 +L 228.87139 30.476685 +L 231.48184 29.710413 +L 233.258632 29.79434 +L 238.792774 29.263361 +L 244.847633 27.742302 +L 250.777169 26.065532 +L 248.713807 25.871907 +L 244.734884 25.61857 +L 247.574009 24.617633 +L 248.453491 23.241387 +L 249.12343 24.460806 +L 249.555027 25.290971 +L 252.657644 24.996516 +L 253.514691 23.358427 +L 252.745976 22.097268 +L 251.342186 21.545581 +L 252.583982 21.0682 +L 254.564014 21.957398 +L 255.354581 21.110899 +L 254.924715 19.79349 +L 256.78417 19.856315 +L 258.942613 19.638827 +L 259.975627 19.016702 +L 259.182353 18.246031 +L 262.141292 18.212538 +L 261.95767 16.685007 +L 263.401259 16.571049 +L 264.522071 15.170066 +L 263.394344 14.266031 +L 265.711093 13.836424 +L 267.449772 13.824024 +L 267.243235 12.804416 +L 269.057293 11.214347 +L 270.839335 10.357462 +L 272.964405 9.438911 +L 271.157806 9.322092 +L 274.005891 9.179782 +L 274.781706 8.905392 +L 278.778698 8.11518 +L 278.830947 7.610473 +L 276.989402 7.34505 +L 273.048944 7.688713 +L 270.571839 8.055948 +L 272.441937 7.417394 +L 272.581431 7.001414 +L 270.713758 7.289576 +L 269.577672 6.832157 +L 267.174732 6.844154 +L 267.345208 6.639683 +L 270.766146 6.691915 +L 273.193624 6.738355 +L 275.025128 6.371501 z -M 324.829474 7.575552 -L 324.437094 7.388459 -L 322.841773 7.233654 -L 322.97529 7.461463 -L 323.559419 7.755531 -L 322.449015 7.469896 -L 321.667364 7.561847 -L 321.207809 7.863691 -L 322.589538 7.782966 -L 323.390339 8.166225 -L 324.885639 8.379911 -L 324.949954 8.124293 -L 324.416147 7.930811 -L 324.623216 7.796006 +M 324.829474 7.575552 +L 324.437094 7.388459 +L 322.841773 7.233654 +L 322.97529 7.461463 +L 323.559419 7.755531 +L 322.449015 7.469896 +L 321.667364 7.561847 +L 321.207809 7.863691 +L 322.589538 7.782966 +L 323.390339 8.166225 +L 324.885639 8.379911 +L 324.949954 8.124293 +L 324.416147 7.930811 +L 324.623216 7.796006 z -M 368.231599 21.631788 -L 363.263556 20.342575 -L 359.811532 18.88105 -L 357.265113 17.068932 -L 354.395759 15.33411 -L 352.444617 13.572973 -L 352.113723 12.584884 -L 351.624297 11.55481 -L 350.500704 11.07518 -L 348.40946 10.624988 -L 348.134248 10.97265 -L 348.951459 11.625728 -L 347.961585 12.18955 -L 348.6142 13.413049 -L 349.201602 14.411892 -L 351.222351 15.088152 -L 352.791737 16.732568 -L 354.032164 16.843286 -L 355.611386 18.492223 -L 357.715631 19.448466 -L 357.551944 19.941728 -L 359.713722 20.913052 -L 362.49841 21.208995 -L 364.453784 22.012212 -L 368.049848 21.868775 +M 368.231599 21.631788 +L 363.263556 20.342575 +L 359.811532 18.88105 +L 357.265113 17.068932 +L 354.395759 15.33411 +L 352.444617 13.572973 +L 352.113723 12.584884 +L 351.624297 11.55481 +L 350.500704 11.07518 +L 348.40946 10.624988 +L 348.134248 10.97265 +L 348.951459 11.625728 +L 347.961585 12.18955 +L 348.6142 13.413049 +L 349.201602 14.411892 +L 351.222351 15.088152 +L 352.791737 16.732568 +L 354.032164 16.843286 +L 355.611386 18.492223 +L 357.715631 19.448466 +L 357.551944 19.941728 +L 359.713722 20.913052 +L 362.49841 21.208995 +L 364.453784 22.012212 +L 368.049848 21.868775 z -M 259.178337 34.68425 -L 256.056755 34.438478 -L 252.184498 35.633207 -L 250.167811 34.882424 -L 246.398305 36.104435 -L 244.573776 34.372864 -L 241.492046 34.621236 -L 239.011521 36.14006 -L 242.610513 36.859012 -L 242.187893 37.586124 -L 238.379912 37.938798 -L 241.833216 39.324345 -L 239.080952 40.378247 -L 244.326541 41.390471 -L 246.886986 41.858094 -L 248.974044 41.436442 -L 255.939369 39.830135 -L 259.495377 37.967983 -L 257.972799 36.235985 +M 259.178337 34.68425 +L 256.056755 34.438478 +L 252.184498 35.633207 +L 250.167811 34.882424 +L 246.398305 36.104435 +L 244.573776 34.372864 +L 241.492046 34.621236 +L 239.011521 36.14006 +L 242.610513 36.859012 +L 242.187893 37.586124 +L 238.379912 37.938798 +L 241.833216 39.324345 +L 239.080952 40.378247 +L 244.326541 41.390471 +L 246.886986 41.858094 +L 248.974044 41.436442 +L 255.939369 39.830135 +L 259.495377 37.967983 +L 257.972799 36.235985 z -M 204.703431 13.836173 -L 207.881233 12.989326 -L 206.416883 13.051725 -L 204.422692 13.512319 -L 200.127317 14.746405 -L 195.294048 16.379156 -L 195.306248 16.586528 -L 193.108694 17.184676 -L 191.465405 17.892756 -L 190.85436 18.387984 -L 189.723404 19.210557 -L 189.431044 20.151951 -L 190.553903 20.191126 -L 192.095106 19.742233 -L 190.788153 20.46892 -L 190.850803 20.702649 -L 188.429801 22.01196 -L 187.006703 22.391973 -L 186.364972 23.356106 -L 184.966438 24.546593 -L 183.65178 25.293097 -L 181.845864 26.237546 -L 176.799831 28.057906 -L 174.40093 29.066382 -L 173.018091 29.900993 -L 171.33814 29.817955 -L 170.369042 29.521859 -L 166.800882 31.023424 -L 165.655475 31.950914 -L 167.853231 32.122998 -L 169.11435 31.465728 -L 167.863753 32.831181 -L 166.6325 34.296419 -L 165.18371 34.89523 -L 163.977474 36.284331 -L 163.244284 38.104825 -L 164.041434 39.611599 -L 165.307124 38.708589 -L 166.836916 37.030639 -L 169.235532 34.70896 -L 168.074442 37.082596 -L 168.165686 37.976031 -L 171.413925 36.286595 -L 174.854081 33.826844 -L 175.58217 33.140257 -L 176.341532 31.857846 -L 177.960094 30.380493 -L 180.49681 29.1478 -L 182.034113 29.086548 -L 179.319398 31.466616 -L 178.673645 32.640944 -L 185.065274 30.309533 -L 188.215 28.824352 -L 187.061483 28.441528 -L 189.687403 26.292106 -L 189.385124 25.604805 -L 190.547527 23.94815 -L 193.819736 23.306246 -L 197.258625 21.396376 -L 198.48788 20.535996 -L 198.878043 19.49909 -L 201.227884 18.251014 -L 199.336857 18.390685 -L 201.282071 17.632467 -L 202.925119 16.663667 -L 204.514416 15.568625 -L 202.359033 16.077398 -L 200.451926 16.282469 -L 203.583517 15.26587 -L 208.136728 13.492873 -L 205.766584 13.782125 -L 201.794455 14.87577 +M 204.703431 13.836173 +L 207.881233 12.989326 +L 206.416883 13.051725 +L 204.422692 13.512319 +L 200.127317 14.746405 +L 195.294048 16.379156 +L 195.306248 16.586528 +L 193.108694 17.184676 +L 191.465405 17.892756 +L 190.85436 18.387984 +L 189.723404 19.210557 +L 189.431044 20.151951 +L 190.553903 20.191126 +L 192.095106 19.742233 +L 190.788153 20.46892 +L 190.850803 20.702649 +L 188.429801 22.01196 +L 187.006703 22.391973 +L 186.364972 23.356106 +L 184.966438 24.546593 +L 183.65178 25.293097 +L 181.845864 26.237546 +L 176.799831 28.057906 +L 174.40093 29.066382 +L 173.018091 29.900993 +L 171.33814 29.817955 +L 170.369042 29.521859 +L 166.800882 31.023424 +L 165.655475 31.950914 +L 167.853231 32.122998 +L 169.11435 31.465728 +L 167.863753 32.831181 +L 166.6325 34.296419 +L 165.18371 34.89523 +L 163.977474 36.284331 +L 163.244284 38.104825 +L 164.041434 39.611599 +L 165.307124 38.708589 +L 166.836916 37.030639 +L 169.235532 34.70896 +L 168.074442 37.082596 +L 168.165686 37.976031 +L 171.413925 36.286595 +L 174.854081 33.826844 +L 175.58217 33.140257 +L 176.341532 31.857846 +L 177.960094 30.380493 +L 180.49681 29.1478 +L 182.034113 29.086548 +L 179.319398 31.466616 +L 178.673645 32.640944 +L 185.065274 30.309533 +L 188.215 28.824352 +L 187.061483 28.441528 +L 189.687403 26.292106 +L 189.385124 25.604805 +L 190.547527 23.94815 +L 193.819736 23.306246 +L 197.258625 21.396376 +L 198.48788 20.535996 +L 198.878043 19.49909 +L 201.227884 18.251014 +L 199.336857 18.390685 +L 201.282071 17.632467 +L 202.925119 16.663667 +L 204.514416 15.568625 +L 202.359033 16.077398 +L 200.451926 16.282469 +L 203.583517 15.26587 +L 208.136728 13.492873 +L 205.766584 13.782125 +L 201.794455 14.87577 z -M 206.657651 15.202612 -L 208.687853 14.154028 -L 208.594321 13.717791 -L 208.166898 13.757494 -L 206.444208 14.287941 -L 204.201291 15.237004 -L 203.99222 15.368554 -L 204.93782 15.297865 -L 205.174354 15.466787 -L 205.398457 15.646411 +M 206.657651 15.202612 +L 208.687853 14.154028 +L 208.594321 13.717791 +L 208.166898 13.757494 +L 206.444208 14.287941 +L 204.201291 15.237004 +L 203.99222 15.368554 +L 204.93782 15.297865 +L 205.174354 15.466787 +L 205.398457 15.646411 z -M 157.273736 34.646644 -L 158.138357 34.196774 -L 157.773648 33.902895 -L 155.535863 34.834375 -L 154.393203 35.519131 -L 154.481049 35.717582 +M 157.273736 34.646644 +L 158.138357 34.196774 +L 157.773648 33.902895 +L 155.535863 34.834375 +L 154.393203 35.519131 +L 154.481049 35.717582 z -M 155.849167 36.487564 -L 156.636293 35.944111 -L 156.562018 35.809943 -L 155.092236 36.452167 -L 154.771386 36.609304 -L 153.573486 37.386848 -L 153.388414 37.685058 +M 155.849167 36.487564 +L 156.636293 35.944111 +L 156.562018 35.809943 +L 155.092236 36.452167 +L 154.771386 36.609304 +L 153.573486 37.386848 +L 153.388414 37.685058 z -M 169.710101 27.472903 -L 169.965607 27.172548 -L 165.74443 29.053071 -L 162.165143 30.750219 -L 159.830088 31.687091 -L 160.444161 31.734562 -L 157.88224 33.13293 -L 160.4864 32.229942 -L 163.117056 31.221971 -L 161.256565 32.344059 -L 160.684843 33.194988 -L 162.408386 32.643911 -L 163.622258 31.756054 -L 163.026151 31.791989 -L 165.127076 30.712169 -L 166.194345 29.813353 -L 167.474567 28.868329 -L 168.54073 28.201841 -L 167.740385 28.437021 +M 169.710101 27.472903 +L 169.965607 27.172548 +L 165.74443 29.053071 +L 162.165143 30.750219 +L 159.830088 31.687091 +L 160.444161 31.734562 +L 157.88224 33.13293 +L 160.4864 32.229942 +L 163.117056 31.221971 +L 161.256565 32.344059 +L 160.684843 33.194988 +L 162.408386 32.643911 +L 163.622258 31.756054 +L 163.026151 31.791989 +L 165.127076 30.712169 +L 166.194345 29.813353 +L 167.474567 28.868329 +L 168.54073 28.201841 +L 167.740385 28.437021 z -M 179.5433 25.990724 -L 181.184471 25.496429 -L 181.858453 25.236531 -L 183.77948 24.379535 -L 184.66438 23.712127 -L 183.632191 23.838291 -L 180.908572 24.878372 -L 178.803549 25.907479 +M 179.5433 25.990724 +L 181.184471 25.496429 +L 181.858453 25.236531 +L 183.77948 24.379535 +L 184.66438 23.712127 +L 183.632191 23.838291 +L 180.908572 24.878372 +L 178.803549 25.907479 z -M 202.833448 13.550996 -L 203.685456 13.42187 -L 207.926815 12.343856 -L 209.169446 11.779343 -L 209.51082 11.483086 -L 208.319059 11.72986 -L 206.353735 12.246903 -L 203.977132 12.943066 -L 199.693193 14.310824 -L 199.366407 14.520605 +M 202.833448 13.550996 +L 203.685456 13.42187 +L 207.926815 12.343856 +L 209.169446 11.779343 +L 209.51082 11.483086 +L 208.319059 11.72986 +L 206.353735 12.246903 +L 203.977132 12.943066 +L 199.693193 14.310824 +L 199.366407 14.520605 z -M 223.907707 7.878927 -L 224.447098 7.610674 -L 222.504081 8.016321 -L 220.845853 8.477705 -L 220.041153 8.829858 -L 217.842956 9.449486 -L 215.423983 10.06831 -L 212.739249 10.803062 -L 211.110576 11.534446 -L 210.553446 11.890315 -L 210.778361 12.107595 -L 211.879426 12.229929 -L 211.517264 12.547776 -L 212.851184 12.419272 -L 214.262319 12.121464 -L 216.19106 11.534478 -L 217.781892 10.920603 -L 217.839255 10.656218 -L 217.15365 10.54671 -L 215.948408 10.651149 -L 216.263082 10.384926 -L 216.433294 10.182616 -L 217.583932 9.819853 -L 218.696765 9.423759 -L 220.526392 9.003201 -L 222.155011 8.537236 -L 222.246421 8.34265 +M 223.907707 7.878927 +L 224.447098 7.610674 +L 222.504081 8.016321 +L 220.845853 8.477705 +L 220.041153 8.829858 +L 217.842956 9.449486 +L 215.423983 10.06831 +L 212.739249 10.803062 +L 211.110576 11.534446 +L 210.553446 11.890315 +L 210.778361 12.107595 +L 211.879426 12.229929 +L 211.517264 12.547776 +L 212.851184 12.419272 +L 214.262319 12.121464 +L 216.19106 11.534478 +L 217.781892 10.920603 +L 217.839255 10.656218 +L 217.15365 10.54671 +L 215.948408 10.651149 +L 216.263082 10.384926 +L 216.433294 10.182616 +L 217.583932 9.819853 +L 218.696765 9.423759 +L 220.526392 9.003201 +L 222.155011 8.537236 +L 222.246421 8.34265 z -M 255.83713 4.271427 -L 255.680162 4.117599 -L 255.577332 3.964676 -L 254.295598 3.940458 -L 254.741111 3.837211 -L 254.15095 3.709763 -L 253.422455 3.691353 -L 252.515713 3.729455 -L 249.783563 4.06499 -L 251.092484 3.805997 -L 251.280625 3.700934 -L 249.364107 3.871967 -L 248.509615 3.868068 -L 248.340953 3.820259 -L 247.407201 3.86135 -L 245.706518 4.117285 -L 244.237975 4.411294 -L 242.232906 4.783397 -L 240.588138 5.187646 -L 241.112169 5.384275 -L 240.7715 5.646896 -L 238.811215 5.860669 -L 239.271458 5.704019 -L 239.303943 5.463388 -L 236.779 5.95663 -L 234.946892 6.436236 -L 233.209869 6.753307 -L 231.925516 6.840558 -L 229.986903 7.161409 -L 229.126467 7.504869 -L 226.093571 8.326276 -L 228.024914 7.608217 -L 227.657087 7.626967 -L 224.135696 8.463944 -L 222.980475 8.538454 -L 220.634783 9.10482 -L 220.435429 9.360916 -L 219.942106 9.660134 -L 221.020503 9.772891 -L 220.126395 10.384416 -L 223.592537 9.903224 -L 224.755797 9.602248 -L 224.193627 9.4839 -L 225.256513 9.200066 -L 226.987414 8.979746 -L 228.972599 8.571277 -L 230.688122 8.387256 -L 232.560237 8.086265 -L 234.719447 7.484987 -L 235.734521 7.352749 -L 236.028502 7.113553 -L 237.247649 7.23969 -L 238.379393 7.077883 -L 239.725761 7.061395 -L 244.027368 6.367057 -L 245.814609 6.201564 -L 249.299157 5.778187 -L 248.632714 5.653601 -L 249.915765 5.505775 -L 251.548369 5.462444 -L 254.232972 5.210657 -L 255.419788 4.983615 -L 256.092759 4.668692 -L 256.106381 4.462217 +M 255.83713 4.271427 +L 255.680162 4.117599 +L 255.577332 3.964676 +L 254.295598 3.940458 +L 254.741111 3.837211 +L 254.15095 3.709763 +L 253.422455 3.691353 +L 252.515713 3.729455 +L 249.783563 4.06499 +L 251.092484 3.805997 +L 251.280625 3.700934 +L 249.364107 3.871967 +L 248.509615 3.868068 +L 248.340953 3.820259 +L 247.407201 3.86135 +L 245.706518 4.117285 +L 244.237975 4.411294 +L 242.232906 4.783397 +L 240.588138 5.187646 +L 241.112169 5.384275 +L 240.7715 5.646896 +L 238.811215 5.860669 +L 239.271458 5.704019 +L 239.303943 5.463388 +L 236.779 5.95663 +L 234.946892 6.436236 +L 233.209869 6.753307 +L 231.925516 6.840558 +L 229.986903 7.161409 +L 229.126467 7.504869 +L 226.093571 8.326276 +L 228.024914 7.608217 +L 227.657087 7.626967 +L 224.135696 8.463944 +L 222.980475 8.538454 +L 220.634783 9.10482 +L 220.435429 9.360916 +L 219.942106 9.660134 +L 221.020503 9.772891 +L 220.126395 10.384416 +L 223.592537 9.903224 +L 224.755797 9.602248 +L 224.193627 9.4839 +L 225.256513 9.200066 +L 226.987414 8.979746 +L 228.972599 8.571277 +L 230.688122 8.387256 +L 232.560237 8.086265 +L 234.719447 7.484987 +L 235.734521 7.352749 +L 236.028502 7.113553 +L 237.247649 7.23969 +L 238.379393 7.077883 +L 239.725761 7.061395 +L 244.027368 6.367057 +L 245.814609 6.201564 +L 249.299157 5.778187 +L 248.632714 5.653601 +L 249.915765 5.505775 +L 251.548369 5.462444 +L 254.232972 5.210657 +L 255.419788 4.983615 +L 256.092759 4.668692 +L 256.106381 4.462217 z -M 236.376539 5.986658 -L 239.609042 5.33926 -L 240.514218 5.059843 -L 241.583037 4.765291 -L 244.263455 4.266173 -L 244.122017 4.148682 -L 242.960624 4.348139 -L 242.682514 4.330322 -L 241.232237 4.514097 -L 239.107196 4.814384 -L 236.818431 5.237219 -L 235.085537 5.613035 -L 235.003114 5.753559 -L 233.736015 5.930852 -L 231.951842 6.267525 -L 229.883297 6.755732 -L 229.185776 7.068496 -L 229.542107 7.15294 -L 233.306346 6.561039 -L 234.851884 6.386317 +M 236.376539 5.986658 +L 239.609042 5.33926 +L 240.514218 5.059843 +L 241.583037 4.765291 +L 244.263455 4.266173 +L 244.122017 4.148682 +L 242.960624 4.348139 +L 242.682514 4.330322 +L 241.232237 4.514097 +L 239.107196 4.814384 +L 236.818431 5.237219 +L 235.085537 5.613035 +L 235.003114 5.753559 +L 233.736015 5.930852 +L 231.951842 6.267525 +L 229.883297 6.755732 +L 229.185776 7.068496 +L 229.542107 7.15294 +L 233.306346 6.561039 +L 234.851884 6.386317 z -M 228.709128 6.775859 -L 230.451122 6.446438 -L 232.28081 6.015305 -L 232.674431 5.905201 -L 233.044299 5.760307 -L 231.015959 6.147767 -L 229.125152 6.544184 -L 227.879092 6.847002 +M 228.709128 6.775859 +L 230.451122 6.446438 +L 232.28081 6.015305 +L 232.674431 5.905201 +L 233.044299 5.760307 +L 231.015959 6.147767 +L 229.125152 6.544184 +L 227.879092 6.847002 z -M 225.902632 7.508972 -L 226.454158 7.400257 -L 227.417067 7.143064 -L 227.690533 6.943038 -L 226.29473 7.252944 -L 225.79751 7.496072 +M 225.902632 7.508972 +L 226.454158 7.400257 +L 227.417067 7.143064 +L 227.690533 6.943038 +L 226.29473 7.252944 +L 225.79751 7.496072 z -M 213.51058 10.485211 -L 215.076006 10.046672 -L 216.862221 9.521889 -L 215.732933 9.691438 -L 213.638088 10.187976 -L 212.207749 10.656903 -L 211.683282 10.921093 +M 213.51058 10.485211 +L 215.076006 10.046672 +L 216.862221 9.521889 +L 215.732933 9.691438 +L 213.638088 10.187976 +L 212.207749 10.656903 +L 211.683282 10.921093 z -M 280.141655 56.065946 -L 276.964936 56.299614 -L 274.907244 56.048852 -L 272.534095 58.471692 -L 271.098644 61.638424 -L 272.271393 63.244756 -L 272.044102 66.30708 -L 273.750186 64.809896 -L 274.582305 65.694624 -L 273.388061 67.121566 -L 273.977634 68.002146 -L 277.441064 68.59671 -L 279.299491 70.671449 -L 278.738554 72.595391 -L 274.319066 72.267555 -L 273.533625 74.458605 -L 275.033067 76.296198 -L 271.719213 77.332241 -L 272.454893 78.708001 -L 277.303481 79.318493 -L 274.44063 80.051714 -L 269.429269 83.702793 -L 271.062729 84.42582 -L 273.442875 83.089516 -L 276.375233 83.509182 -L 278.591508 81.868226 -L 280.041215 82.566676 -L 285.49683 81.613849 -L 289.749641 81.647049 -L 292.557263 79.815073 -L 291.264988 78.029066 -L 292.816281 77.019826 -L 293.116671 74.843926 -L 289.424007 74.207244 -L 288.555361 72.882352 -L 286.742348 69.116817 -L 284.755551 68.590642 -L 282.127031 64.455392 -L 279.231375 64.241843 -L 281.902689 61.443176 -L 282.73624 58.939811 -L 279.79597 58.912686 -L 277.02269 59.315768 +M 280.141655 56.065946 +L 276.964936 56.299614 +L 274.907244 56.048852 +L 272.534095 58.471692 +L 271.098644 61.638424 +L 272.271393 63.244756 +L 272.044102 66.30708 +L 273.750186 64.809896 +L 274.582305 65.694624 +L 273.388061 67.121566 +L 273.977634 68.002146 +L 277.441064 68.59671 +L 279.299491 70.671449 +L 278.738554 72.595391 +L 274.319066 72.267555 +L 273.533625 74.458605 +L 275.033067 76.296198 +L 271.719213 77.332241 +L 272.454893 78.708001 +L 277.303481 79.318493 +L 274.44063 80.051714 +L 269.429269 83.702793 +L 271.062729 84.42582 +L 273.442875 83.089516 +L 276.375233 83.509182 +L 278.591508 81.868226 +L 280.041215 82.566676 +L 285.49683 81.613849 +L 289.749641 81.647049 +L 292.557263 79.815073 +L 291.264988 78.029066 +L 292.816281 77.019826 +L 293.116671 74.843926 +L 289.424007 74.207244 +L 288.555361 72.882352 +L 286.742348 69.116817 +L 284.755551 68.590642 +L 282.127031 64.455392 +L 279.231375 64.241843 +L 281.902689 61.443176 +L 282.73624 58.939811 +L 279.79597 58.912686 +L 277.02269 59.315768 z -M 136.490517 74.883035 -L 139.475309 72.899053 -L 140.69681 72.043645 -L 140.021842 71.794672 -L 137.386901 72.738456 -L 134.454375 74.493917 -L 127.489125 79.665668 -L 124.100228 81.547253 -L 123.972614 82.60496 -L 121.768615 83.685353 -L 121.085324 84.770124 -L 124.446612 85.366319 -L 126.626714 85.419568 -L 127.714673 86.520323 -L 124.922846 88.031687 -L 125.972424 88.304306 -L 130.86959 85.480919 -L 131.039745 85.999954 -L 128.160925 88.884686 -L 128.939904 89.727809 -L 129.981933 89.701877 -L 133.434819 86.63143 -L 134.646653 84.347644 -L 135.984839 82.450059 -L 134.081026 82.887004 -L 136.920718 80.403888 -L 135.951622 79.111627 -L 134.320776 79.844728 -L 133.535044 78.69257 -L 135.282574 77.580864 -L 134.747155 76.694959 -L 132.497669 77.692014 +M 136.490517 74.883035 +L 139.475309 72.899053 +L 140.69681 72.043645 +L 140.021842 71.794672 +L 137.386901 72.738456 +L 134.454375 74.493917 +L 127.489125 79.665668 +L 124.100228 81.547253 +L 123.972614 82.60496 +L 121.768615 83.685353 +L 121.085324 84.770124 +L 124.446612 85.366319 +L 126.626714 85.419568 +L 127.714673 86.520323 +L 124.922846 88.031687 +L 125.972424 88.304306 +L 130.86959 85.480919 +L 131.039745 85.999954 +L 128.160925 88.884686 +L 128.939904 89.727809 +L 129.981933 89.701877 +L 133.434819 86.63143 +L 134.646653 84.347644 +L 135.984839 82.450059 +L 134.081026 82.887004 +L 136.920718 80.403888 +L 135.951622 79.111627 +L 134.320776 79.844728 +L 133.535044 78.69257 +L 135.282574 77.580864 +L 134.747155 76.694959 +L 132.497669 77.692014 z -M 110.491213 87.370887 -L 111.557835 85.542 -L 109.973799 86.539115 -L 109.247827 87.805963 -L 109.839288 89.681856 -L 110.642231 89.54576 -L 112.75471 88.198156 -L 111.182119 88.047987 +M 110.491213 87.370887 +L 111.557835 85.542 +L 109.973799 86.539115 +L 109.247827 87.805963 +L 109.839288 89.681856 +L 110.642231 89.54576 +L 112.75471 88.198156 +L 111.182119 88.047987 z -M 121.82218 78.887657 -L 122.39218 78.251118 -L 122.257232 76.569779 -L 121.218084 75.389781 -L 120.441691 75.585333 -L 120.139341 77.423366 -L 121.01105 78.825717 +M 121.82218 78.887657 +L 122.39218 78.251118 +L 122.257232 76.569779 +L 121.218084 75.389781 +L 120.441691 75.585333 +L 120.139341 77.423366 +L 121.01105 78.825717 z -M 267.161778 76.37076 -L 269.848115 73.374387 -L 269.665571 70.992272 -L 271.521933 68.75172 -L 268.713598 66.720591 -L 266.303362 66.827163 -L 263.874051 68.311636 -L 259.429759 70.822135 -L 260.302878 74.234553 -L 257.156819 77.749923 -L 261.408434 78.331728 +M 267.161778 76.37076 +L 269.848115 73.374387 +L 269.665571 70.992272 +L 271.521933 68.75172 +L 268.713598 66.720591 +L 266.303362 66.827163 +L 263.874051 68.311636 +L 259.429759 70.822135 +L 260.302878 74.234553 +L 257.156819 77.749923 +L 261.408434 78.331728 z -M 37.059273 164.628887 -L 37.281663 162.297001 -L 37.027553 162.815045 -L 36.58833 164.770331 -L 35.437637 167.133105 -L 35.345928 167.786301 +M 37.059273 164.628887 +L 37.281663 162.297001 +L 37.027553 162.815045 +L 36.58833 164.770331 +L 35.437637 167.133105 +L 35.345928 167.786301 z -M 36.233874 164.360564 -L 36.779723 163.175284 -L 36.240129 162.787555 -L 35.650659 162.96791 -L 34.893536 164.665322 +M 36.233874 164.360564 +L 36.779723 163.175284 +L 36.240129 162.787555 +L 35.650659 162.96791 +L 34.893536 164.665322 z -M 30.621132 177.363322 -L 31.77647 174.685537 -L 33.143526 170.742848 -L 32.944286 170.441995 -L 31.42816 173.263823 -L 31.188791 174.740548 -L 30.281488 177.497131 +M 30.621132 177.363322 +L 31.77647 174.685537 +L 33.143526 170.742848 +L 32.944286 170.441995 +L 31.42816 173.263823 +L 31.188791 174.740548 +L 30.281488 177.497131 z -M 26.729806 181.125911 -L 26.643423 179.171542 -L 26.097931 178.806543 -L 25.670658 178.131963 -L 25.30728 178.543623 -L 24.686963 178.683794 -L 24.041579 179.381975 -L 23.393984 180.23154 -L 22.607309 181.820692 -L 21.800144 183.045718 -L 21.805828 183.661003 -L 22.238754 183.351261 -L 22.766341 182.272401 -L 23.004245 182.374539 -L 24.39268 180.247843 -L 24.905206 180.878478 -L 24.186178 181.889849 -L 24.043993 182.938213 -L 24.686003 184.183819 -L 24.531857 185.284107 -L 24.816477 186.913001 -L 25.391067 186.95649 -L 24.590488 189.72595 -L 24.412758 191.224095 -L 24.936386 191.797594 -L 24.911533 193.187456 -L 23.280405 195.559465 -L 24.960553 195.672333 -L 25.618014 196.323014 -L 26.507577 196.355498 -L 27.550471 196.017052 -L 28.093206 194.953311 -L 27.839534 192.717819 -L 27.031017 192.231108 -L 27.604406 190.931863 -L 27.281185 189.734997 -L 26.893081 189.669021 -L 27.036248 187.30301 -L 27.32375 184.069007 -L 27.426773 182.832688 -L 26.37229 182.988142 +M 26.729806 181.125911 +L 26.643423 179.171542 +L 26.097931 178.806543 +L 25.670658 178.131963 +L 25.30728 178.543623 +L 24.686963 178.683794 +L 24.041579 179.381975 +L 23.393984 180.23154 +L 22.607309 181.820692 +L 21.800144 183.045718 +L 21.805828 183.661003 +L 22.238754 183.351261 +L 22.766341 182.272401 +L 23.004245 182.374539 +L 24.39268 180.247843 +L 24.905206 180.878478 +L 24.186178 181.889849 +L 24.043993 182.938213 +L 24.686003 184.183819 +L 24.531857 185.284107 +L 24.816477 186.913001 +L 25.391067 186.95649 +L 24.590488 189.72595 +L 24.412758 191.224095 +L 24.936386 191.797594 +L 24.911533 193.187456 +L 23.280405 195.559465 +L 24.960553 195.672333 +L 25.618014 196.323014 +L 26.507577 196.355498 +L 27.550471 196.017052 +L 28.093206 194.953311 +L 27.839534 192.717819 +L 27.031017 192.231108 +L 27.604406 190.931863 +L 27.281185 189.734997 +L 26.893081 189.669021 +L 27.036248 187.30301 +L 27.32375 184.069007 +L 27.426773 182.832688 +L 26.37229 182.988142 z -M 29.568545 197.544248 -L 28.792083 197.094057 -L 28.036126 198.318044 -L 28.653714 199.309781 -L 28.037181 201.128434 -L 28.015728 203.379953 -L 27.160544 204.303464 -L 26.235481 203.619141 -L 25.234067 202.587563 -L 24.625885 204.095092 -L 24.852426 205.803362 -L 25.760692 205.093647 -L 26.496811 205.683505 -L 27.274263 205.538192 -L 27.99968 206.61722 -L 27.654221 208.019016 -L 27.814656 208.886274 -L 29.43519 205.752232 -L 30.167714 205.193313 -L 30.047751 206.417409 -L 30.75607 206.271881 -L 31.320807 205.466831 -L 31.791962 205.82356 -L 32.624776 205.800879 -L 33.120289 207.030221 -L 34.370528 205.219209 -L 34.068888 203.2714 -L 33.36734 202.926427 -L 33.881399 201.514088 -L 32.941189 201.397484 -L 33.202927 199.634511 -L 32.737987 199.651977 -L 32.215244 198.195932 -L 31.033438 197.869502 -L 30.571766 198.631626 +M 29.568545 197.544248 +L 28.792083 197.094057 +L 28.036126 198.318044 +L 28.653714 199.309781 +L 28.037181 201.128434 +L 28.015728 203.379953 +L 27.160544 204.303464 +L 26.235481 203.619141 +L 25.234067 202.587563 +L 24.625885 204.095092 +L 24.852426 205.803362 +L 25.760692 205.093647 +L 26.496811 205.683505 +L 27.274263 205.538192 +L 27.99968 206.61722 +L 27.654221 208.019016 +L 27.814656 208.886274 +L 29.43519 205.752232 +L 30.167714 205.193313 +L 30.047751 206.417409 +L 30.75607 206.271881 +L 31.320807 205.466831 +L 31.791962 205.82356 +L 32.624776 205.800879 +L 33.120289 207.030221 +L 34.370528 205.219209 +L 34.068888 203.2714 +L 33.36734 202.926427 +L 33.881399 201.514088 +L 32.941189 201.397484 +L 33.202927 199.634511 +L 32.737987 199.651977 +L 32.215244 198.195932 +L 31.033438 197.869502 +L 30.571766 198.631626 z -M 38.899648 208.108035 -L 38.831473 207.084189 -L 37.969516 206.466805 -L 36.434919 206.126589 -L 35.958445 206.774347 -L 35.448152 208.855328 -L 36.594794 208.909394 -L 38.037433 209.224956 +M 38.899648 208.108035 +L 38.831473 207.084189 +L 37.969516 206.466805 +L 36.434919 206.126589 +L 35.958445 206.774347 +L 35.448152 208.855328 +L 36.594794 208.909394 +L 38.037433 209.224956 z -M 21.019737 205.382353 -L 21.829759 205.578021 -L 22.055681 204.198234 -L 21.837794 202.833667 -L 21.267723 202.132256 -L 21.088659 201.878821 -L 20.56362 202.040928 -L 20.09333 203.084867 -L 20.103984 205.062922 -L 20.44394 206.060723 +M 21.019737 205.382353 +L 21.829759 205.578021 +L 22.055681 204.198234 +L 21.837794 202.833667 +L 21.267723 202.132256 +L 21.088659 201.878821 +L 20.56362 202.040928 +L 20.09333 203.084867 +L 20.103984 205.062922 +L 20.44394 206.060723 z -M 40.174361 249.641582 -L 40.865862 245.959497 -L 40.392314 245.706958 -L 38.925312 246.13425 -L 38.651844 248.101317 -L 37.757742 249.356403 -L 38.108344 249.87188 +M 40.174361 249.641582 +L 40.865862 245.959497 +L 40.392314 245.706958 +L 38.925312 246.13425 +L 38.651844 248.101317 +L 37.757742 249.356403 +L 38.108344 249.87188 z -M 502.654347 370.915332 -L 502.455979 367.819151 -L 501.971537 365.853679 -L 501.203246 363.892423 -L 499.776158 366.160939 -L 499.212841 369.064828 -L 496.821046 372.57851 -L 495.541964 372.136664 -L 495.60461 374.141127 -L 494.159557 376.639512 -L 490.863336 379.828725 -L 488.410449 382.707425 -L 486.923781 382.904468 -L 485.49519 383.872307 -L 483.395034 385.00786 -L 481.648463 385.329319 -L 480.552231 388.356963 -L 478.770974 391.084153 -L 478.058504 395.363334 -L 478.000259 398.237157 -L 478.240658 400.360264 -L 477.178993 403.320521 -L 474.636284 406.930682 -L 474.211542 408.449845 -L 472.43347 409.367914 -L 470.905565 412.687326 -L 470.269024 415.904991 -L 470.36287 419.377516 -L 469.327981 423.300062 -L 469.466539 425.554305 -L 471.509862 426.90773 -L 472.962826 427.866647 -L 476.378264 425.760671 -L 479.285383 424.481671 -L 482.45591 419.201268 -L 485.675191 412.842772 -L 490.196978 404.026921 -L 493.499549 397.516359 -L 496.135825 391.964073 -L 497.306703 387.972242 -L 498.424347 386.797091 -L 499.167097 384.768959 -L 499.359547 381.323793 -L 500.252229 379.858054 -L 500.696895 382.528464 -L 501.553979 381.076495 -L 502.356327 378.765425 -L 502.021775 376.621295 +M 502.654347 370.915332 +L 502.455979 367.819151 +L 501.971537 365.853679 +L 501.203246 363.892423 +L 499.776158 366.160939 +L 499.212841 369.064828 +L 496.821046 372.57851 +L 495.541964 372.136664 +L 495.60461 374.141127 +L 494.159557 376.639512 +L 490.863336 379.828725 +L 488.410449 382.707425 +L 486.923781 382.904468 +L 485.49519 383.872307 +L 483.395034 385.00786 +L 481.648463 385.329319 +L 480.552231 388.356963 +L 478.770974 391.084153 +L 478.058504 395.363334 +L 478.000259 398.237157 +L 478.240658 400.360264 +L 477.178993 403.320521 +L 474.636284 406.930682 +L 474.211542 408.449845 +L 472.43347 409.367914 +L 470.905565 412.687326 +L 470.269024 415.904991 +L 470.36287 419.377516 +L 469.327981 423.300062 +L 469.466539 425.554305 +L 471.509862 426.90773 +L 472.962826 427.866647 +L 476.378264 425.760671 +L 479.285383 424.481671 +L 482.45591 419.201268 +L 485.675191 412.842772 +L 490.196978 404.026921 +L 493.499549 397.516359 +L 496.135825 391.964073 +L 497.306703 387.972242 +L 498.424347 386.797091 +L 499.167097 384.768959 +L 499.359547 381.323793 +L 500.252229 379.858054 +L 500.696895 382.528464 +L 501.553979 381.076495 +L 502.356327 378.765425 +L 502.021775 376.621295 z -M 463.316589 512.324218 -L 465.261143 510.819689 -L 465.904089 510.227941 -L 465.286733 510.08671 -L 465.643941 509.258249 -L 464.840825 509.929292 -L 463.205304 511.273579 -L 461.337923 512.934022 +M 463.316589 512.324218 +L 465.261143 510.819689 +L 465.904089 510.227941 +L 465.286733 510.08671 +L 465.643941 509.258249 +L 464.840825 509.929292 +L 463.205304 511.273579 +L 461.337923 512.934022 z -M 133.713214 519.507549 -L 134.08068 520.548505 -L 131.883538 519.609958 -L 132.09994 521.092384 -L 134.409809 522.519547 -L 134.162445 521.410431 -L 136.062681 522.532379 -L 137.212854 521.973195 -L 136.539241 521.020494 +M 133.713214 519.507549 +L 134.08068 520.548505 +L 131.883538 519.609958 +L 132.09994 521.092384 +L 134.409809 522.519547 +L 134.162445 521.410431 +L 136.062681 522.532379 +L 137.212854 521.973195 +L 136.539241 521.020494 z -M 125.231007 521.583066 -L 124.012205 521.062022 -L 124.593572 522.035091 -L 126.865819 523.924505 -L 128.121758 525.099853 -L 125.508318 523.755244 -L 121.708433 521.477161 -L 120.220077 520.654092 -L 125.70329 524.23521 -L 128.686092 526.00221 -L 132.012529 527.85461 -L 133.580475 528.498584 -L 135.468969 529.486238 -L 137.023879 530.040942 -L 136.760693 529.398681 -L 135.594709 528.372915 -L 137.511949 529.45278 -L 138.432258 529.534114 -L 137.106714 528.274087 -L 134.497835 527.260979 -L 130.756911 525.277015 -L 127.387694 523.019829 -L 125.231566 521.583442 +M 125.231007 521.583066 +L 124.012205 521.062022 +L 124.593572 522.035091 +L 126.865819 523.924505 +L 128.121758 525.099853 +L 125.508318 523.755244 +L 121.708433 521.477161 +L 120.220077 520.654092 +L 125.70329 524.23521 +L 128.686092 526.00221 +L 132.012529 527.85461 +L 133.580475 528.498584 +L 135.468969 529.486238 +L 137.023879 530.040942 +L 136.760693 529.398681 +L 135.594709 528.372915 +L 137.511949 529.45278 +L 138.432258 529.534114 +L 137.106714 528.274087 +L 134.497835 527.260979 +L 130.756911 525.277015 +L 127.387694 523.019829 +L 125.231566 521.583442 z -M 570.963206 260.838383 -L 571.119233 259.239221 -L 570.596131 253.991174 -L 569.517937 249.027129 -L 568.617192 245.735004 -L 567.594624 243.278908 -L 568.459804 251.518889 -L 569.542412 258.593811 -L 570.387255 262.353156 +M 570.963206 260.838383 +L 571.119233 259.239221 +L 570.596131 253.991174 +L 569.517937 249.027129 +L 568.617192 245.735004 +L 567.594624 243.278908 +L 568.459804 251.518889 +L 569.542412 258.593811 +L 570.387255 262.353156 z -M 348.535442 129.450208 -L 345.711213 129.884827 -L 341.882923 130.429159 -L 337.309899 130.13663 -L 337.110707 132.311468 -L 342.893469 134.35629 -L 344.95163 134.771881 -L 348.216569 136.309387 -L 348.624215 134.099834 -L 347.798829 132.799888 +M 348.535442 129.450208 +L 345.711213 129.884827 +L 341.882923 130.429159 +L 337.309899 130.13663 +L 337.110707 132.311468 +L 342.893469 134.35629 +L 344.95163 134.771881 +L 348.216569 136.309387 +L 348.624215 134.099834 +L 347.798829 132.799888 z -M 382.002534 139.221501 -L 381.80549 141.079775 -L 386.610726 141.744229 -L 386.772262 142.460197 -L 392.023556 141.869951 -L 392.107117 140.575642 -L 390.249139 141.179183 -L 390.119056 140.424305 -L 387.275591 140.232283 -L 384.445774 140.593852 +M 382.002534 139.221501 +L 381.80549 141.079775 +L 386.610726 141.744229 +L 386.772262 142.460197 +L 392.023556 141.869951 +L 392.107117 140.575642 +L 390.249139 141.179183 +L 390.119056 140.424305 +L 387.275591 140.232283 +L 384.445774 140.593852 z -M 420.776861 137.483202 -L 418.186292 138.943988 -L 415.697739 139.027337 -L 415.579681 140.092775 -L 415.343922 140.130073 -L 413.75351 140.380053 -L 415.185355 142.069306 -L 417.088887 142.535789 -L 419.975721 140.576729 -L 419.739348 140.236487 -L 419.188536 139.445219 +M 420.776861 137.483202 +L 418.186292 138.943988 +L 415.697739 139.027337 +L 415.579681 140.092775 +L 415.343922 140.130073 +L 413.75351 140.380053 +L 415.185355 142.069306 +L 417.088887 142.535789 +L 419.975721 140.576729 +L 419.739348 140.236487 +L 419.188536 139.445219 z -M 322.677962 117.619901 -L 320.965191 118.904332 -L 318.874797 118.727577 -L 320.005514 121.049675 -L 320.725889 126.025267 -L 322.313288 127.109897 -L 323.719659 125.69926 -L 325.499539 125.935993 -L 325.313294 120.476754 +M 322.677962 117.619901 +L 320.965191 118.904332 +L 318.874797 118.727577 +L 320.005514 121.049675 +L 320.725889 126.025267 +L 322.313288 127.109897 +L 323.719659 125.69926 +L 325.499539 125.935993 +L 325.313294 120.476754 z -M 323.460572 113.80555 -L 322.359818 110.402232 -L 320.220719 111.949711 -L 319.670159 113.441323 -L 320.866414 116.131858 -L 322.661187 116.930705 +M 323.460572 113.80555 +L 322.359818 110.402232 +L 320.220719 111.949711 +L 319.670159 113.441323 +L 320.866414 116.131858 +L 322.661187 116.930705 z -M 323.734852 65.072312 -L 322.403537 63.502761 -L 318.637699 64.622596 -L 319.35486 65.938405 -L 322.771282 67.70357 +M 323.734852 65.072312 +L 322.403537 63.502761 +L 318.637699 64.622596 +L 319.35486 65.938405 +L 322.771282 67.70357 z " style="fill:#6aaed6;stroke:#6aaed6;"/> - <path clip-path="url(#p9f79fa544f)" d="M 198.023757 561.52834 -L 199.650869 562.076466 -L 198.914025 561.862046 -L 199.083468 561.92581 -L 199.003106 561.904277 + <path clip-path="url(#p9f79fa544f)" d="M 198.023757 561.52834 +L 199.650869 562.076466 +L 198.914025 561.862046 +L 199.083468 561.92581 +L 199.003106 561.904277 z " style="fill:#6aaed6;stroke:#6aaed6;"/> - <path clip-path="url(#p9f79fa544f)" d="M 187.249452 18.607528 -L 185.393936 19.363646 -L 180.769597 21.098912 -L 180.781273 21.090429 -L 184.015363 19.848979 + <path clip-path="url(#p9f79fa544f)" d="M 187.249452 18.607528 +L 185.393936 19.363646 +L 180.769597 21.098912 +L 180.781273 21.090429 +L 184.015363 19.848979 z " style="fill:#6aaed6;stroke:#6aaed6;"/> - <path clip-path="url(#p9f79fa544f)" d="M 373.586265 562.818152 -L 375.187036 562.316592 -L 377.005585 561.713332 -L 376.238963 561.893385 -L 377.872663 561.31785 -L 380.004093 560.632393 -L 381.674069 560.108877 -L 377.541962 561.695045 -L 377.353995 561.755526 -L 376.35308 562.076786 -L 376.353078 562.076787 + <path clip-path="url(#p9f79fa544f)" d="M 373.586265 562.818152 +L 375.187036 562.316592 +L 377.005585 561.713332 +L 376.238963 561.893385 +L 377.872663 561.31785 +L 380.004093 560.632393 +L 381.674069 560.108877 +L 377.541962 561.695045 +L 377.353995 561.755526 +L 376.35308 562.076786 +L 376.353078 562.076787 z " style="fill:#6aaed6;stroke:#6aaed6;"/> </g> <g id="patch_3"> - <path d="M 576 288 -L 574.422306 318.104197 -L 569.706509 347.878567 -L 561.904277 376.996894 -L 551.101092 405.140153 -L 537.415316 432 -L 520.996894 457.282153 -L 502.02571 480.709615 -L 480.709615 502.02571 -L 457.282153 520.996894 -L 432 537.415316 -L 405.140153 551.101092 -L 376.996894 561.904277 -L 347.878567 569.706509 -L 318.104197 574.422306 -L 288 576 -L 257.895803 574.422306 -L 228.121433 569.706509 -L 199.003106 561.904277 -L 170.859847 551.101092 -L 144 537.415316 -L 118.717847 520.996894 -L 95.290385 502.02571 -L 73.97429 480.709615 -L 55.003106 457.282153 -L 38.584684 432 -L 24.898908 405.140153 -L 14.095723 376.996894 -L 6.293491 347.878567 -L 1.577694 318.104197 -L 0 288 -L 1.577694 257.895803 -L 6.293491 228.121433 -L 14.095723 199.003106 -L 24.898908 170.859847 -L 38.584684 144 -L 55.003106 118.717847 -L 73.97429 95.290385 -L 95.290385 73.97429 -L 118.717847 55.003106 -L 144 38.584684 -L 170.859847 24.898908 -L 199.003106 14.095723 -L 228.121433 6.293491 -L 257.895803 1.577694 -L 288 0 -L 318.104197 1.577694 -L 347.878567 6.293491 -L 376.996894 14.095723 -L 405.140153 24.898908 -L 432 38.584684 -L 457.282153 55.003106 -L 480.709615 73.97429 -L 502.02571 95.290385 -L 520.996894 118.717847 -L 537.415316 144 -L 551.101092 170.859847 -L 561.904277 199.003106 -L 569.706509 228.121433 -L 574.422306 257.895803 -L 576 288 -L 576 288 + <path d="M 576 288 +L 574.422306 318.104197 +L 569.706509 347.878567 +L 561.904277 376.996894 +L 551.101092 405.140153 +L 537.415316 432 +L 520.996894 457.282153 +L 502.02571 480.709615 +L 480.709615 502.02571 +L 457.282153 520.996894 +L 432 537.415316 +L 405.140153 551.101092 +L 376.996894 561.904277 +L 347.878567 569.706509 +L 318.104197 574.422306 +L 288 576 +L 257.895803 574.422306 +L 228.121433 569.706509 +L 199.003106 561.904277 +L 170.859847 551.101092 +L 144 537.415316 +L 118.717847 520.996894 +L 95.290385 502.02571 +L 73.97429 480.709615 +L 55.003106 457.282153 +L 38.584684 432 +L 24.898908 405.140153 +L 14.095723 376.996894 +L 6.293491 347.878567 +L 1.577694 318.104197 +L 0 288 +L 1.577694 257.895803 +L 6.293491 228.121433 +L 14.095723 199.003106 +L 24.898908 170.859847 +L 38.584684 144 +L 55.003106 118.717847 +L 73.97429 95.290385 +L 95.290385 73.97429 +L 118.717847 55.003106 +L 144 38.584684 +L 170.859847 24.898908 +L 199.003106 14.095723 +L 228.121433 6.293491 +L 257.895803 1.577694 +L 288 0 +L 318.104197 1.577694 +L 347.878567 6.293491 +L 376.996894 14.095723 +L 405.140153 24.898908 +L 432 38.584684 +L 457.282153 55.003106 +L 480.709615 73.97429 +L 502.02571 95.290385 +L 520.996894 118.717847 +L 537.415316 144 +L 551.101092 170.859847 +L 561.904277 199.003106 +L 569.706509 228.121433 +L 574.422306 257.895803 +L 576 288 +L 576 288 " style="fill:none;"/> </g> <g id="text_1"> <!-- Psy --> <defs> - <path d="M 53.90625 72.90625 -Q 59.796875 72.90625 64.09375 71.296875 -Q 68.40625 69.703125 70.59375 67.046875 -Q 72.796875 64.40625 73.75 61.703125 -Q 74.703125 59 74.703125 56 -Q 74.703125 52.59375 73.75 48.796875 -Q 72.796875 45 70.640625 40.890625 -Q 68.5 36.796875 65.390625 33.5 -Q 62.296875 30.203125 57.4375 28.09375 -Q 52.59375 26 46.796875 26 -L 28.09375 26 -L 22.59375 0 -L 7.59375 0 -L 23.09375 72.90625 + <path d="M 53.90625 72.90625 +Q 59.796875 72.90625 64.09375 71.296875 +Q 68.40625 69.703125 70.59375 67.046875 +Q 72.796875 64.40625 73.75 61.703125 +Q 74.703125 59 74.703125 56 +Q 74.703125 52.59375 73.75 48.796875 +Q 72.796875 45 70.640625 40.890625 +Q 68.5 36.796875 65.390625 33.5 +Q 62.296875 30.203125 57.4375 28.09375 +Q 52.59375 26 46.796875 26 +L 28.09375 26 +L 22.59375 0 +L 7.59375 0 +L 23.09375 72.90625 z -M 46.09375 38.5 -Q 50 38.5 52.84375 39.84375 -Q 55.703125 41.203125 57.09375 43.453125 -Q 58.5 45.703125 59.140625 47.953125 -Q 59.796875 50.203125 59.796875 52.5 -Q 59.796875 56 57.296875 58.203125 -Q 54.796875 60.40625 50.796875 60.40625 -L 35.40625 60.40625 -L 30.796875 38.5 +M 46.09375 38.5 +Q 50 38.5 52.84375 39.84375 +Q 55.703125 41.203125 57.09375 43.453125 +Q 58.5 45.703125 59.140625 47.953125 +Q 59.796875 50.203125 59.796875 52.5 +Q 59.796875 56 57.296875 58.203125 +Q 54.796875 60.40625 50.796875 60.40625 +L 35.40625 60.40625 +L 30.796875 38.5 z " id="FreeSansBoldOblique-80"/> - <path d="M 12.703125 34.5 -Q 12.703125 43.296875 19.953125 49.09375 -Q 27.203125 54.90625 38.703125 54.90625 -Q 48.09375 54.90625 53.5 51.046875 -Q 58.90625 47.203125 58.90625 40.40625 -Q 58.90625 38.796875 58.40625 36.59375 -L 44.90625 36.59375 -Q 45.203125 37.90625 45.203125 38.5 -Q 45.203125 40.90625 42.84375 42.296875 -Q 40.5 43.703125 36.40625 43.703125 -Q 32.40625 43.703125 30 42.25 -Q 27.59375 40.796875 27.59375 38.40625 -Q 27.59375 36.90625 28.9375 35.953125 -Q 30.296875 35 34.09375 33.90625 -L 43.703125 31.09375 -Q 55.90625 27.40625 55.90625 18.703125 -Q 55.90625 9.09375 48.34375 3.390625 -Q 40.796875 -2.296875 28.09375 -2.296875 -Q 21.796875 -2.296875 17.1875 -0.796875 -Q 12.59375 0.703125 10.34375 3.140625 -Q 8.09375 5.59375 7.046875 7.9375 -Q 6 10.296875 6 12.703125 -Q 6 13.703125 6.40625 15.703125 -L 20.09375 15.703125 -Q 20.5 8.90625 30.703125 8.90625 -Q 35.296875 8.90625 38.1875 10.453125 -Q 41.09375 12 41.09375 14.40625 -Q 41.09375 16.09375 39.640625 17.09375 -Q 38.203125 18.09375 33.5 19.59375 -L 25.59375 22.09375 -Q 19 24.296875 15.84375 27.296875 -Q 12.703125 30.296875 12.703125 34.5 + <path d="M 12.703125 34.5 +Q 12.703125 43.296875 19.953125 49.09375 +Q 27.203125 54.90625 38.703125 54.90625 +Q 48.09375 54.90625 53.5 51.046875 +Q 58.90625 47.203125 58.90625 40.40625 +Q 58.90625 38.796875 58.40625 36.59375 +L 44.90625 36.59375 +Q 45.203125 37.90625 45.203125 38.5 +Q 45.203125 40.90625 42.84375 42.296875 +Q 40.5 43.703125 36.40625 43.703125 +Q 32.40625 43.703125 30 42.25 +Q 27.59375 40.796875 27.59375 38.40625 +Q 27.59375 36.90625 28.9375 35.953125 +Q 30.296875 35 34.09375 33.90625 +L 43.703125 31.09375 +Q 55.90625 27.40625 55.90625 18.703125 +Q 55.90625 9.09375 48.34375 3.390625 +Q 40.796875 -2.296875 28.09375 -2.296875 +Q 21.796875 -2.296875 17.1875 -0.796875 +Q 12.59375 0.703125 10.34375 3.140625 +Q 8.09375 5.59375 7.046875 7.9375 +Q 6 10.296875 6 12.703125 +Q 6 13.703125 6.40625 15.703125 +L 20.09375 15.703125 +Q 20.5 8.90625 30.703125 8.90625 +Q 35.296875 8.90625 38.1875 10.453125 +Q 41.09375 12 41.09375 14.40625 +Q 41.09375 16.09375 39.640625 17.09375 +Q 38.203125 18.09375 33.5 19.59375 +L 25.59375 22.09375 +Q 19 24.296875 15.84375 27.296875 +Q 12.703125 30.296875 12.703125 34.5 z " id="FreeSansBoldOblique-115"/> - <path d="M 50.90625 54 -L 65.296875 54 -L 29.5 -9.796875 -Q 27 -14.5 25.09375 -16.40625 -Q 19.40625 -21.90625 8.09375 -21.90625 -Q 5.40625 -21.90625 3.703125 -21.5 -L 6 -10.59375 -Q 7.09375 -10.796875 8.796875 -10.796875 -Q 14 -10.796875 17.046875 -8.296875 -Q 20.09375 -5.796875 20.09375 -1.5 -Q 18.796875 9.203125 16.046875 28.453125 -Q 13.296875 47.703125 12.40625 54 -L 27.796875 54 -L 31.40625 14.703125 + <path d="M 50.90625 54 +L 65.296875 54 +L 29.5 -9.796875 +Q 27 -14.5 25.09375 -16.40625 +Q 19.40625 -21.90625 8.09375 -21.90625 +Q 5.40625 -21.90625 3.703125 -21.5 +L 6 -10.59375 +Q 7.09375 -10.796875 8.796875 -10.796875 +Q 14 -10.796875 17.046875 -8.296875 +Q 20.09375 -5.796875 20.09375 -1.5 +Q 18.796875 9.203125 16.046875 28.453125 +Q 13.296875 47.703125 12.40625 54 +L 27.796875 54 +L 31.40625 14.703125 z " id="FreeSansBoldOblique-121"/> </defs> diff --git a/psyplot_gui/icons/logo.svg.license b/psyplot_gui/icons/logo.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..b21fae9960bf621e598d171baf4de286e59ac4e1 --- /dev/null +++ b/psyplot_gui/icons/logo.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/minus.png b/psyplot_gui/icons/minus.png index 1b27257ba99eb75cca628a5b94ee2dcf2e395134..79f7b7cf1c8a14a5514221f08802555c3e680a11 100644 Binary files a/psyplot_gui/icons/minus.png and b/psyplot_gui/icons/minus.png differ diff --git a/psyplot_gui/icons/minus.png.license b/psyplot_gui/icons/minus.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/minus.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/minus.svg b/psyplot_gui/icons/minus.svg new file mode 100644 index 0000000000000000000000000000000000000000..0d44471f557af261fbd71d65605cd06cd024423c --- /dev/null +++ b/psyplot_gui/icons/minus.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#a51d2d" d="M432 256c0 17.7-14.3 32-32 32L48 288c-17.7 0-32-14.3-32-32s14.3-32 32-32l352 0c17.7 0 32 14.3 32 32z"/></svg> diff --git a/psyplot_gui/icons/minus.svg.license b/psyplot_gui/icons/minus.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/minus.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/minusminus.png b/psyplot_gui/icons/minusminus.png index ee5d9f91a265933690b2ce9e82e6c8e40489158b..01969514667233cbba6e7da0982d60feedee5cd6 100644 Binary files a/psyplot_gui/icons/minusminus.png and b/psyplot_gui/icons/minusminus.png differ diff --git a/psyplot_gui/icons/minusminus.png.license b/psyplot_gui/icons/minusminus.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/minusminus.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/minusminus.svg b/psyplot_gui/icons/minusminus.svg new file mode 100644 index 0000000000000000000000000000000000000000..cfaa12baeb6cc671fab34ff668bb4d01f6a58163 --- /dev/null +++ b/psyplot_gui/icons/minusminus.svg @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 448 512" + version="1.1" + id="svg1" + sodipodi:docname="minusminus.svg" + inkscape:version="1.3.1 (9b9bdc1480, 2023-11-25, custom)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs1" /> + <sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + inkscape:zoom="3.703125" + inkscape:cx="223.86498" + inkscape:cy="256" + inkscape:window-width="3840" + inkscape:window-height="2096" + inkscape:window-x="3840" + inkscape:window-y="27" + inkscape:window-maximized="1" + inkscape:current-layer="svg1" /> + <!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--> + <path + fill="#a51d2d" + d="m 324.23536,256 c 0,17.7 -10.59559,32 -23.71041,32 H 39.710413 C 26.595591,288 16,273.7 16,256 16,238.3 26.595591,224 39.710413,224 H 300.52495 c 13.11482,0 23.71041,14.3 23.71041,32 z" + id="path1" + style="stroke-width:0.860785" /> + <path + fill="#a51d2d" + d="m 418.61497,339.18793 c 0,17.7 -10.59559,32 -23.71041,32 H 134.09002 c -13.11481,0 -23.71041,-14.3 -23.71041,-32 0,-17.7 10.5956,-32 23.71041,-32 h 260.81454 c 13.11482,0 23.71041,14.3 23.71041,32 z" + id="path1-2" + style="stroke-width:0.860785" /> +</svg> diff --git a/psyplot_gui/icons/minusminus.svg.license b/psyplot_gui/icons/minusminus.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/minusminus.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/next.png b/psyplot_gui/icons/next.png index 611530c6df20cf873992a6d2686ea293e26cf637..e7ce7d23f4fa826d5a3054871d1bda85fe9e18d7 100644 Binary files a/psyplot_gui/icons/next.png and b/psyplot_gui/icons/next.png differ diff --git a/psyplot_gui/icons/next.png.license b/psyplot_gui/icons/next.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/next.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/next.svg b/psyplot_gui/icons/next.svg new file mode 100644 index 0000000000000000000000000000000000000000..245a685b3e1a15ae13c90a5f942a9c909dee4f9b --- /dev/null +++ b/psyplot_gui/icons/next.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"/></svg> diff --git a/psyplot_gui/icons/next.svg.license b/psyplot_gui/icons/next.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/next.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/plus.png b/psyplot_gui/icons/plus.png index 2e93407e19898239a66957fd2b988d4469fad81a..71681af89a6b42a43f207add4982add9a39062d4 100644 Binary files a/psyplot_gui/icons/plus.png and b/psyplot_gui/icons/plus.png differ diff --git a/psyplot_gui/icons/plus.png.license b/psyplot_gui/icons/plus.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/plus.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/plus.svg b/psyplot_gui/icons/plus.svg new file mode 100644 index 0000000000000000000000000000000000000000..d662a265f8ffd14fd271fc498b2eaf6b42fbc7ca --- /dev/null +++ b/psyplot_gui/icons/plus.svg @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + viewBox="0 0 448 512" + version="1.1" + id="svg1" + sodipodi:docname="plus.svg" + inkscape:version="1.3.1 (9b9bdc1480, 2023-11-25, custom)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs1" /> + <sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="3.703125" + inkscape:cx="223.86498" + inkscape:cy="256" + inkscape:window-width="3840" + inkscape:window-height="2096" + inkscape:window-x="3840" + inkscape:window-y="27" + inkscape:window-maximized="1" + inkscape:current-layer="svg1" /> + <!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--> + <path + fill="#63e6be" + d="M 186.1421,70.685613 C 186.1421,58.137634 176.00447,48 163.45649,48 150.90851,48 140.77087,58.137634 140.77087,70.685613 V 172.77087 H 38.685613 C 26.137634,172.77087 16,182.90851 16,195.45649 16,208.00447 26.137634,218.1421 38.685613,218.1421 H 140.77087 v 102.08526 c 0,12.54798 10.13764,22.68561 22.68562,22.68561 12.54798,0 22.68561,-10.13763 22.68561,-22.68561 V 218.1421 h 102.08526 c 12.54798,0 22.68561,-10.13763 22.68561,-22.68561 0,-12.54798 -10.13763,-22.68562 -22.68561,-22.68562 H 186.1421 Z" + id="path1" + style="stroke-width:0.708925;stroke:#000000;stroke-opacity:1" /> + <path + fill="#63e6be" + d="m 273.36032,157.2226 c 0,-12.54798 -10.13763,-22.68561 -22.68561,-22.68561 -12.54798,0 -22.68562,10.13763 -22.68562,22.68561 V 259.30786 H 125.90383 c -12.54798,0 -22.68561,10.13764 -22.68561,22.68562 0,12.54798 10.13763,22.68561 22.68561,22.68561 h 102.08526 v 102.08526 c 0,12.54798 10.13764,22.68561 22.68562,22.68561 12.54798,0 22.68561,-10.13763 22.68561,-22.68561 V 304.67909 h 102.08526 c 12.54798,0 22.68561,-10.13763 22.68561,-22.68561 0,-12.54798 -10.13763,-22.68562 -22.68561,-22.68562 H 273.36032 Z" + id="path1-7" + style="stroke-width:0.708925;stroke:#000000;stroke-opacity:1" /> +</svg> diff --git a/psyplot_gui/icons/plus.svg.license b/psyplot_gui/icons/plus.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/plus.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/plusplus.png.license b/psyplot_gui/icons/plusplus.png.license new file mode 100644 index 0000000000000000000000000000000000000000..b21fae9960bf621e598d171baf4de286e59ac4e1 --- /dev/null +++ b/psyplot_gui/icons/plusplus.png.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/previous.png b/psyplot_gui/icons/previous.png index f69bf9ce88835df7234d4b4380f858621eea4a5e..42a7aa8e16d94b8ae4d4c734d1b97fc73505f3af 100644 Binary files a/psyplot_gui/icons/previous.png and b/psyplot_gui/icons/previous.png differ diff --git a/psyplot_gui/icons/previous.png.license b/psyplot_gui/icons/previous.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/previous.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/previous.svg b/psyplot_gui/icons/previous.svg new file mode 100644 index 0000000000000000000000000000000000000000..763ce7eba30a7daa93110a0a56497563d9e3577d --- /dev/null +++ b/psyplot_gui/icons/previous.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/></svg> diff --git a/psyplot_gui/icons/previous.svg.license b/psyplot_gui/icons/previous.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/previous.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/rcParams.png b/psyplot_gui/icons/rcParams.png index 1b17c78696aae46bb926c4dd3ebd834311cf28c7..e0beeab1034f0e64bd8b28c604e508f491c5ce3f 100644 Binary files a/psyplot_gui/icons/rcParams.png and b/psyplot_gui/icons/rcParams.png differ diff --git a/psyplot_gui/icons/rcParams.png.license b/psyplot_gui/icons/rcParams.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/rcParams.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/rcParams.svg b/psyplot_gui/icons/rcParams.svg new file mode 100644 index 0000000000000000000000000000000000000000..a866019225e054cb6387c1f225bc7e6beba6bfda --- /dev/null +++ b/psyplot_gui/icons/rcParams.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z"/></svg> diff --git a/psyplot_gui/icons/rcParams.svg.license b/psyplot_gui/icons/rcParams.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/rcParams.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/refresh.png b/psyplot_gui/icons/refresh.png index d339514072f2e9b164e546965f7a1eaa9d2cf9d6..0cdcc5b9119f8d97aac50ee0f2eae0c5b38f56c0 100644 Binary files a/psyplot_gui/icons/refresh.png and b/psyplot_gui/icons/refresh.png differ diff --git a/psyplot_gui/icons/refresh.png.license b/psyplot_gui/icons/refresh.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/refresh.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/refresh.svg b/psyplot_gui/icons/refresh.svg new file mode 100644 index 0000000000000000000000000000000000000000..5cb67f73056e2ba3923326adfeef213a4b1ee441 --- /dev/null +++ b/psyplot_gui/icons/refresh.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M105.1 202.6c7.7-21.8 20.2-42.3 37.8-59.8c62.5-62.5 163.8-62.5 226.3 0L386.3 160H352c-17.7 0-32 14.3-32 32s14.3 32 32 32H463.5c0 0 0 0 0 0h.4c17.7 0 32-14.3 32-32V80c0-17.7-14.3-32-32-32s-32 14.3-32 32v35.2L414.4 97.6c-87.5-87.5-229.3-87.5-316.8 0C73.2 122 55.6 150.7 44.8 181.4c-5.9 16.7 2.9 34.9 19.5 40.8s34.9-2.9 40.8-19.5zM39 289.3c-5 1.5-9.8 4.2-13.7 8.2c-4 4-6.7 8.8-8.1 14c-.3 1.2-.6 2.5-.8 3.8c-.3 1.7-.4 3.4-.4 5.1V432c0 17.7 14.3 32 32 32s32-14.3 32-32V396.9l17.6 17.5 0 0c87.5 87.4 229.3 87.4 316.7 0c24.4-24.4 42.1-53.1 52.9-83.7c5.9-16.7-2.9-34.9-19.5-40.8s-34.9 2.9-40.8 19.5c-7.7 21.8-20.2 42.3-37.8 59.8c-62.5 62.5-163.8 62.5-226.3 0l-.1-.1L125.6 352H160c17.7 0 32-14.3 32-32s-14.3-32-32-32H48.4c-1.6 0-3.2 .1-4.8 .3s-3.1 .5-4.6 1z"/></svg> diff --git a/psyplot_gui/icons/refresh.svg.license b/psyplot_gui/icons/refresh.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/refresh.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/run_arrow.png b/psyplot_gui/icons/run_arrow.png index 4a58c376e4072bc6ce39d1deff214ed68cfdcfc7..5c44c29eaba86b0922b1fdb0e2aa88a4e67b02c5 100644 Binary files a/psyplot_gui/icons/run_arrow.png and b/psyplot_gui/icons/run_arrow.png differ diff --git a/psyplot_gui/icons/run_arrow.png.license b/psyplot_gui/icons/run_arrow.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/run_arrow.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/run_arrow.svg b/psyplot_gui/icons/run_arrow.svg new file mode 100644 index 0000000000000000000000000000000000000000..44bf83946e6e8be8ae2c9e359a040511d1c74d1e --- /dev/null +++ b/psyplot_gui/icons/run_arrow.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#63E6BE" d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zM188.3 147.1c7.6-4.2 16.8-4.1 24.3 .5l144 88c7.1 4.4 11.5 12.1 11.5 20.5s-4.4 16.1-11.5 20.5l-144 88c-7.4 4.5-16.7 4.7-24.3 .5s-12.3-12.2-12.3-20.9V168c0-8.7 4.7-16.7 12.3-20.9z"/></svg> diff --git a/psyplot_gui/icons/run_arrow.svg.license b/psyplot_gui/icons/run_arrow.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/run_arrow.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/valid.png b/psyplot_gui/icons/valid.png index cd1016734af6f763af5f8f57dc85c78edc849081..b661054aef63d4474d2b3a2c83b71b4ffd866c5c 100644 Binary files a/psyplot_gui/icons/valid.png and b/psyplot_gui/icons/valid.png differ diff --git a/psyplot_gui/icons/valid.png.license b/psyplot_gui/icons/valid.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/valid.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/valid.svg b/psyplot_gui/icons/valid.svg new file mode 100644 index 0000000000000000000000000000000000000000..2d78b95a2e6655dc87858fce52fa193ae3a7b5b6 --- /dev/null +++ b/psyplot_gui/icons/valid.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#63E6BE" d="M438.6 105.4c12.5 12.5 12.5 32.8 0 45.3l-256 256c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L160 338.7 393.4 105.4c12.5-12.5 32.8-12.5 45.3 0z"/></svg> diff --git a/psyplot_gui/icons/valid.svg.license b/psyplot_gui/icons/valid.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/valid.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/warning.png b/psyplot_gui/icons/warning.png index 9801d124850b4d65c636fecd6013f88728d5854e..41dcd85bbeaedef0e6953d598b4b9ccce42ada63 100644 Binary files a/psyplot_gui/icons/warning.png and b/psyplot_gui/icons/warning.png differ diff --git a/psyplot_gui/icons/warning.png.license b/psyplot_gui/icons/warning.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/warning.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/warning.svg b/psyplot_gui/icons/warning.svg new file mode 100644 index 0000000000000000000000000000000000000000..433c3ed58a96b72e79421e6d9f7e471e4bf0a72a --- /dev/null +++ b/psyplot_gui/icons/warning.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480H40c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24V296c0 13.3 10.7 24 24 24s24-10.7 24-24V184c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg> diff --git a/psyplot_gui/icons/warning.svg.license b/psyplot_gui/icons/warning.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/warning.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/world.png b/psyplot_gui/icons/world.png index 36dde376431cc982de2740a774f6b980b4fbec9a..86838e1cb53a9526ff2b04ce4181ec54c5e08dbd 100644 Binary files a/psyplot_gui/icons/world.png and b/psyplot_gui/icons/world.png differ diff --git a/psyplot_gui/icons/world.png.license b/psyplot_gui/icons/world.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/world.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/world.svg b/psyplot_gui/icons/world.svg new file mode 100644 index 0000000000000000000000000000000000000000..1502b795c6d8991dd2c261b0902e520044549774 --- /dev/null +++ b/psyplot_gui/icons/world.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M57.7 193l9.4 16.4c8.3 14.5 21.9 25.2 38 29.8L163 255.7c17.2 4.9 29 20.6 29 38.5v39.9c0 11 6.2 21 16 25.9s16 14.9 16 25.9v39c0 15.6 14.9 26.9 29.9 22.6c16.1-4.6 28.6-17.5 32.7-33.8l2.8-11.2c4.2-16.9 15.2-31.4 30.3-40l8.1-4.6c15-8.5 24.2-24.5 24.2-41.7v-8.3c0-12.7-5.1-24.9-14.1-33.9l-3.9-3.9c-9-9-21.2-14.1-33.9-14.1H257c-11.1 0-22.1-2.9-31.8-8.4l-34.5-19.7c-4.3-2.5-7.6-6.5-9.2-11.2c-3.2-9.6 1.1-20 10.2-24.5l5.9-3c6.6-3.3 14.3-3.9 21.3-1.5l23.2 7.7c8.2 2.7 17.2-.4 21.9-7.5c4.7-7 4.2-16.3-1.2-22.8l-13.6-16.3c-10-12-9.9-29.5 .3-41.3l15.7-18.3c8.8-10.3 10.2-25 3.5-36.7l-2.4-4.2c-3.5-.2-6.9-.3-10.4-.3C163.1 48 84.4 108.9 57.7 193zM464 256c0-36.8-9.6-71.4-26.4-101.5L412 164.8c-15.7 6.3-23.8 23.8-18.5 39.8l16.9 50.7c3.5 10.4 12 18.3 22.6 20.9l29.1 7.3c1.2-9 1.8-18.2 1.8-27.5zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256z"/></svg> diff --git a/psyplot_gui/icons/world.svg.license b/psyplot_gui/icons/world.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/world.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/world_red.png b/psyplot_gui/icons/world_red.png index f12058e4d2b392b2421caf417688b2d5e3a677d9..9971a65f864a6f13c7e5d38a886122a4e388ffee 100644 Binary files a/psyplot_gui/icons/world_red.png and b/psyplot_gui/icons/world_red.png differ diff --git a/psyplot_gui/icons/world_red.png.license b/psyplot_gui/icons/world_red.png.license new file mode 100644 index 0000000000000000000000000000000000000000..eaac5c9450db81966b187cdda35d3f43eb4f9755 --- /dev/null +++ b/psyplot_gui/icons/world_red.png.license @@ -0,0 +1,4 @@ +SPDX-FileCopyrightText: 2024 Helmholtz-Zentrum hereon GmbH +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/icons/world_red.svg.license b/psyplot_gui/icons/world_red.svg.license new file mode 100644 index 0000000000000000000000000000000000000000..23f28910dac5f4962f3dcf8b477d686be3ca370f --- /dev/null +++ b/psyplot_gui/icons/world_red.svg.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Fonticons, Inc. (https://fontawesome.com) + +SPDX-License-Identifier: CC-BY-4.0 diff --git a/psyplot_gui/main.py b/psyplot_gui/main.py index a7a372227345d3be09d6038773a02364703bde43..a730bc9bb66a768bbc89f309d894d28774af8322 100644 --- a/psyplot_gui/main.py +++ b/psyplot_gui/main.py @@ -6,75 +6,76 @@ There is no need to import this module because the :class:`GuiProject` class defined here replaces the project class in the :mod:`psyplot.project` module.""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only -import sys -import six -import socket import errno -import pickle +import logging import os -from pkg_resources import iter_entry_points +import pickle +import socket +import sys +from collections import OrderedDict, defaultdict from functools import partial -from collections import defaultdict, OrderedDict -import matplotlib as mpl -from psyplot.compat.pycompat import get_default_value -from psyplot_gui import rcParams from threading import Thread -import logging -# change backend here before the project module is imported -backend = rcParams['backend'] -if backend is not None: - if backend == 'psyplot': - backend = 'module://psyplot_gui.backend' - mpl.use(backend) +import matplotlib as mpl +import psyplot +import psyplot.data as psyd +import psyplot.plotter as psyp +import psyplot.project as psy +import six +import xarray as xr +from psyplot.config.rcsetup import get_configdir +from psyplot.docstring import docstrings +from psyplot.utils import get_default_value -from psyplot_gui.console import ConsoleWidget +import psyplot_gui +from psyplot_gui import rcParams +from psyplot_gui.common import PyErrorMessage, StreamToLogger, get_icon from psyplot_gui.compat.qtcompat import ( - QMainWindow, QApplication, Qt, QMenu, QAction, QDesktopWidget, QLabel, - QFileDialog, QKeySequence, QtCore, with_qt5, QMessageBox, QIcon, - QInputDialog, QActionGroup) + QAction, + QActionGroup, + QApplication, + QDesktopWidget, + QFileDialog, + QIcon, + QInputDialog, + QKeySequence, + QLabel, + QMainWindow, + QMenu, + QMessageBox, + Qt, + QtCore, + with_qt5, +) +from psyplot_gui.console import ConsoleWidget from psyplot_gui.content_widget import ( - ProjectContentWidget, DatasetTree, FiguresTree) -from psyplot_gui.plot_creator import PlotCreator -from psyplot_gui.help_explorer import HelpExplorer + DatasetTree, + FiguresTree, + ProjectContentWidget, +) from psyplot_gui.dataframeeditor import DataFrameEditor +from psyplot_gui.dependencies import DependenciesDialog from psyplot_gui.fmt_widget import FormatoptionWidget -from psyplot_gui.common import PyErrorMessage, get_icon, StreamToLogger +from psyplot_gui.help_explorer import HelpExplorer +from psyplot_gui.plot_creator import PlotCreator from psyplot_gui.preferences import ( - Prefences, GuiRcParamsWidget, PsyRcParamsWidget) -from psyplot_gui.dependencies import DependenciesDialog + GuiRcParamsWidget, + Prefences, + PsyRcParamsWidget, +) -from psyplot.docstring import docstrings -import psyplot.plotter as psyp -import psyplot.project as psy -import psyplot -from psyplot.config.rcsetup import get_configdir -import psyplot.data as psyd -import psyplot_gui -import xarray as xr +# change backend here before the project module is imported +backend = rcParams["backend"] +if backend is not None: + if backend == "psyplot": + backend = "module://psyplot_gui.backend" + mpl.use(backend) #: The :class:`PyQt5.QtWidgets.QMainWindow` of the graphical user interface @@ -87,7 +88,6 @@ def _set_mainwindow(obj): class MainWindow(QMainWindow): - #: A signal that is emmitted when the a signal is received through the #: open_files_server open_external = QtCore.pyqtSignal(list) @@ -135,15 +135,16 @@ class MainWindow(QMainWindow): #: The key for the central widget for the main window in the #: :attr:`plugins` dictionary - central_widget_key = 'console' + central_widget_key = "console" @property def logger(self): """The logger of this instance""" - return logging.getLogger('%s.%s' % (self.__class__.__module__, - self.__class__.__name__)) + return logging.getLogger( + "%s.%s" % (self.__class__.__module__, self.__class__.__name__) + ) - @docstrings.get_sections(base='MainWindow') + @docstrings.get_sections(base="MainWindow") @docstrings.dedent def __init__(self, show=True): """ @@ -157,24 +158,28 @@ class MainWindow(QMainWindow): if sys.stderr is None: sys.stderr = StreamToLogger(self.logger) super(MainWindow, self).__init__() - self.setWindowIcon(QIcon(get_icon('logo.png'))) + self.setWindowIcon(QIcon(get_icon("logo.png"))) #: list of figures from the psyplot backend self.figures = [] self.error_msg = PyErrorMessage(self) self.setDockOptions( - QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks | - QMainWindow.AllowTabbedDocks) + QMainWindow.AnimatedDocks + | QMainWindow.AllowNestedDocks + | QMainWindow.AllowTabbedDocks + ) #: Inprocess console self.console = ConsoleWidget(self) self.project_actions = {} self.config_pages = [] - self.open_file_options = OrderedDict([ - ('new psyplot plot from dataset', self.open_external_files), - ('new psyplot project', partial(self.open_external_files, [])), - ]) + self.open_file_options = OrderedDict( + [ + ("new psyplot plot from dataset", self.open_external_files), + ("new psyplot project", partial(self.open_external_files, [])), + ] + ) # --------------------------------------------------------------------- # ----------------------------- Menus --------------------------------- @@ -184,143 +189,165 @@ class MainWindow(QMainWindow): # --------------------------- New plot -------------------------------- - self.file_menu = QMenu('File', parent=self) - self.new_plot_action = QAction('New plot', self) + self.file_menu = QMenu("File", parent=self) + self.new_plot_action = QAction("New plot", self) self.new_plot_action.setStatusTip( - 'Use an existing dataset (or open a new one) to create one or ' - 'more plots') + "Use an existing dataset (or open a new one) to create one or " + "more plots" + ) self.register_shortcut(self.new_plot_action, QKeySequence.New) self.new_plot_action.triggered.connect(lambda: self.new_plots(True)) self.file_menu.addAction(self.new_plot_action) # --------------------------- Open project ---------------------------- - self.open_project_menu = QMenu('Open project', self) + self.open_project_menu = QMenu("Open project", self) self.file_menu.addMenu(self.open_project_menu) - self.open_mp_action = QAction('New main project', self) + self.open_mp_action = QAction("New main project", self) self.register_shortcut(self.open_mp_action, QKeySequence.Open) - self.open_mp_action.setStatusTip('Open a new main project') + self.open_mp_action.setStatusTip("Open a new main project") self.open_mp_action.triggered.connect(self.open_mp) self.open_project_menu.addAction(self.open_mp_action) - self.open_sp_action = QAction('Add to current', self) + self.open_sp_action = QAction("Add to current", self) self.register_shortcut( - self.open_sp_action, QKeySequence( - 'Ctrl+Shift+O', QKeySequence.NativeText)) + self.open_sp_action, + QKeySequence("Ctrl+Shift+O", QKeySequence.NativeText), + ) self.open_sp_action.setStatusTip( - 'Load a project as a sub project and add it to the current main ' - 'project') + "Load a project as a sub project and add it to the current main " + "project" + ) self.open_sp_action.triggered.connect(self.open_sp) self.open_project_menu.addAction(self.open_sp_action) # ---------------------- load preset menu ----------------------------- - self.load_preset_menu = QMenu('Load preset', parent=self) + self.load_preset_menu = QMenu("Load preset", parent=self) self.file_menu.addMenu(self.load_preset_menu) self.load_sp_preset_action = self.load_preset_menu.addAction( - "For selection", self.load_sp_preset) + "For selection", self.load_sp_preset + ) self.load_sp_preset_action.setStatusTip( - "Load a preset for the selected project") + "Load a preset for the selected project" + ) self.load_mp_preset_action = self.load_preset_menu.addAction( - "For full project", self.load_mp_preset) + "For full project", self.load_mp_preset + ) self.load_sp_preset_action.setStatusTip( - "Load a preset for the full project") + "Load a preset for the full project" + ) # ----------------------- Save project -------------------------------- - self.save_project_menu = QMenu('Save', parent=self) + self.save_project_menu = QMenu("Save", parent=self) self.file_menu.addMenu(self.save_project_menu) - self.save_mp_action = QAction('Full psyplot project', self) + self.save_mp_action = QAction("Full psyplot project", self) self.save_mp_action.setStatusTip( - 'Save the entire project into a pickle file') + "Save the entire project into a pickle file" + ) self.register_shortcut(self.save_mp_action, QKeySequence.Save) self.save_mp_action.triggered.connect(self.save_mp) self.save_project_menu.addAction(self.save_mp_action) - self.save_sp_action = QAction('Selected psyplot project', self) + self.save_sp_action = QAction("Selected psyplot project", self) self.save_sp_action.setStatusTip( - 'Save the selected sub project into a pickle file') + "Save the selected sub project into a pickle file" + ) self.save_sp_action.triggered.connect(self.save_sp) self.save_project_menu.addAction(self.save_sp_action) # ------------------------ Save project as ---------------------------- - self.save_project_as_menu = QMenu('Save as', parent=self) + self.save_project_as_menu = QMenu("Save as", parent=self) self.file_menu.addMenu(self.save_project_as_menu) - self.save_mp_as_action = QAction('Full psyplot project', self) + self.save_mp_as_action = QAction("Full psyplot project", self) self.save_mp_as_action.setStatusTip( - 'Save the entire project into a pickle file') - self.register_shortcut(self.save_mp_as_action, - QKeySequence.SaveAs) + "Save the entire project into a pickle file" + ) + self.register_shortcut(self.save_mp_as_action, QKeySequence.SaveAs) self.save_mp_as_action.triggered.connect( - partial(self.save_mp, new_fname=True)) + partial(self.save_mp, new_fname=True) + ) self.save_project_as_menu.addAction(self.save_mp_as_action) - self.save_sp_as_action = QAction('Selected psyplot project', self) + self.save_sp_as_action = QAction("Selected psyplot project", self) self.save_sp_as_action.setStatusTip( - 'Save the selected sub project into a pickle file') + "Save the selected sub project into a pickle file" + ) self.save_sp_as_action.triggered.connect( - partial(self.save_sp, new_fname=True)) + partial(self.save_sp, new_fname=True) + ) self.save_project_as_menu.addAction(self.save_sp_as_action) # ------------------------ Save preset -------------------------------- - self.save_preset_menu = QMenu('Save preset', parent=self) + self.save_preset_menu = QMenu("Save preset", parent=self) self.file_menu.addMenu(self.save_preset_menu) self.save_sp_preset_action = self.save_preset_menu.addAction( - "Selection", self.save_sp_preset) + "Selection", self.save_sp_preset + ) self.save_sp_preset_action.setStatusTip( - "Save the formatoptions of the selected project as a preset") + "Save the formatoptions of the selected project as a preset" + ) self.save_mp_preset_action = self.save_preset_menu.addAction( - "Full project", self.save_mp_preset) + "Full project", self.save_mp_preset + ) self.save_sp_preset_action.setStatusTip( - "Save the formatoptions of the full project as a preset") + "Save the formatoptions of the full project as a preset" + ) # -------------------------- Pack project ----------------------------- - self.pack_project_menu = QMenu('Zip project files', parent=self) + self.pack_project_menu = QMenu("Zip project files", parent=self) self.file_menu.addMenu(self.pack_project_menu) - self.pack_mp_action = QAction('Full psyplot project', self) + self.pack_mp_action = QAction("Full psyplot project", self) self.pack_mp_action.setStatusTip( - 'Pack all the data of the main project into one folder') + "Pack all the data of the main project into one folder" + ) self.pack_mp_action.triggered.connect(partial(self.save_mp, pack=True)) self.pack_project_menu.addAction(self.pack_mp_action) - self.pack_sp_action = QAction('Selected psyplot project', self) + self.pack_sp_action = QAction("Selected psyplot project", self) self.pack_sp_action.setStatusTip( - 'Pack all the data of the current sub project into one folder') + "Pack all the data of the current sub project into one folder" + ) self.pack_sp_action.triggered.connect(partial(self.save_sp, pack=True)) self.pack_project_menu.addAction(self.pack_sp_action) # ------------------------ Export figures ----------------------------- - self.export_project_menu = QMenu('Export figures', parent=self) + self.export_project_menu = QMenu("Export figures", parent=self) self.file_menu.addMenu(self.export_project_menu) - self.export_mp_action = QAction('Full psyplot project', self) + self.export_mp_action = QAction("Full psyplot project", self) self.export_mp_action.setStatusTip( - 'Pack all the data of the main project into one folder') + "Pack all the data of the main project into one folder" + ) self.export_mp_action.triggered.connect(self.export_mp) self.register_shortcut( - self.export_mp_action, QKeySequence( - 'Ctrl+E', QKeySequence.NativeText)) + self.export_mp_action, + QKeySequence("Ctrl+E", QKeySequence.NativeText), + ) self.export_project_menu.addAction(self.export_mp_action) - self.export_sp_action = QAction('Selected psyplot project', self) + self.export_sp_action = QAction("Selected psyplot project", self) self.export_sp_action.setStatusTip( - 'Pack all the data of the current sub project into one folder') + "Pack all the data of the current sub project into one folder" + ) self.register_shortcut( - self.export_sp_action, QKeySequence( - 'Ctrl+Shift+E', QKeySequence.NativeText)) + self.export_sp_action, + QKeySequence("Ctrl+Shift+E", QKeySequence.NativeText), + ) self.export_sp_action.triggered.connect(self.export_sp) self.export_project_menu.addAction(self.export_sp_action) @@ -328,77 +355,82 @@ class MainWindow(QMainWindow): self.file_menu.addSeparator() - self.close_project_menu = QMenu('Close project', parent=self) + self.close_project_menu = QMenu("Close project", parent=self) self.file_menu.addMenu(self.close_project_menu) - self.close_mp_action = QAction('Full psyplot project', self) + self.close_mp_action = QAction("Full psyplot project", self) self.register_shortcut( - self.close_mp_action, QKeySequence( - 'Ctrl+Shift+W', QKeySequence.NativeText)) + self.close_mp_action, + QKeySequence("Ctrl+Shift+W", QKeySequence.NativeText), + ) self.close_mp_action.setStatusTip( - 'Close the main project and delete all data and plots out of ' - 'memory') + "Close the main project and delete all data and plots out of " + "memory" + ) self.close_mp_action.triggered.connect( - lambda: psy.close(psy.gcp(True).num)) + lambda: psy.close(psy.gcp(True).num) + ) self.close_project_menu.addAction(self.close_mp_action) - self.close_sp_action = QAction('Selected psyplot project', self) + self.close_sp_action = QAction("Selected psyplot project", self) self.close_sp_action.setStatusTip( - 'Close the selected arrays project and delete all data and plots ' - 'out of memory') + "Close the selected arrays project and delete all data and plots " + "out of memory" + ) self.register_shortcut(self.close_sp_action, QKeySequence.Close) self.close_sp_action.triggered.connect( - lambda: psy.gcp().close(True, True)) + lambda: psy.gcp().close(True, True) + ) self.close_project_menu.addAction(self.close_sp_action) # ----------------------------- Quit ---------------------------------- - if sys.platform != 'darwin': # mac os makes this anyway - self.quit_action = QAction('Quit', self) + if sys.platform != "darwin": # mac os makes this anyway + self.quit_action = QAction("Quit", self) self.quit_action.triggered.connect(self.close) self.quit_action.triggered.connect( - QtCore.QCoreApplication.instance().quit) - self.register_shortcut( - self.quit_action, QKeySequence.Quit) + QtCore.QCoreApplication.instance().quit + ) + self.register_shortcut(self.quit_action, QKeySequence.Quit) self.file_menu.addAction(self.quit_action) self.menuBar().addMenu(self.file_menu) # ######################## Console menu ############################### - self.console_menu = QMenu('Console', self) + self.console_menu = QMenu("Console", self) self.console_menu.addActions(self.console.actions()) self.menuBar().addMenu(self.console_menu) # ######################## Windows menu ############################### - self.windows_menu = QMenu('Windows', self) + self.windows_menu = QMenu("Windows", self) self.menuBar().addMenu(self.windows_menu) # ############################ Help menu ############################## - self.help_menu = QMenu('Help', parent=self) + self.help_menu = QMenu("Help", parent=self) self.menuBar().addMenu(self.help_menu) # -------------------------- Preferences ------------------------------ - self.help_action = QAction('Preferences', self) + self.help_action = QAction("Preferences", self) self.help_action.triggered.connect(lambda: self.edit_preferences(True)) - self.register_shortcut(self.help_action, - QKeySequence.Preferences) + self.register_shortcut(self.help_action, QKeySequence.Preferences) self.help_menu.addAction(self.help_action) # ---------------------------- About ---------------------------------- - self.about_action = QAction('About', self) + self.about_action = QAction("About", self) self.about_action.triggered.connect(self.about) self.help_menu.addAction(self.about_action) # ---------------------------- Dependencies --------------------------- - self.dependencies_action = QAction('Dependencies', self) + self.dependencies_action = QAction("Dependencies", self) self.dependencies_action.triggered.connect( - lambda: self.show_dependencies(True)) + lambda: self.show_dependencies(True) + ) self.help_menu.addAction(self.dependencies_action) self.dockwidgets = [] @@ -414,27 +446,31 @@ class MainWindow(QMainWindow): self.figures_tree = FiguresTree(parent=self) #: help explorer self.help_explorer = help_explorer = HelpExplorer(parent=self) - if 'HTML help' in help_explorer.viewers and help_explorer.viewers[ - 'HTML help'].sphinx_thread is not None: + if ( + "HTML help" in help_explorer.viewers + and help_explorer.viewers["HTML help"].sphinx_thread is not None + ): help_explorer.viewers[ - 'HTML help'].sphinx_thread.html_ready.connect( - self.focus_on_console) + "HTML help" + ].sphinx_thread.html_ready.connect(self.focus_on_console) #: the DataFrameEditor widgets self.dataframeeditors = [] #: general formatoptions widget self.fmt_widget = FormatoptionWidget( - parent=self, help_explorer=help_explorer, - console=self.console) + parent=self, help_explorer=help_explorer, console=self.console + ) # load plugin widgets - self.plugins = plugins = OrderedDict([ - ('console', self.console), - ('project_content', self.project_content), - ('ds_tree', self.ds_tree), - ('figures_tree', self.figures_tree), - ('help_explorer', self.help_explorer), - ('fmt_widget', self.fmt_widget), - ]) + self.plugins = plugins = OrderedDict( + [ + ("console", self.console), + ("project_content", self.project_content), + ("ds_tree", self.ds_tree), + ("figures_tree", self.figures_tree), + ("help_explorer", self.help_explorer), + ("fmt_widget", self.fmt_widget), + ] + ) self.default_plugins = list(plugins) for plugin_name, w_class in six.iteritems(rcParams.load_plugins()): plugins[plugin_name] = w_class(parent=self) @@ -443,23 +479,24 @@ class MainWindow(QMainWindow): psy.Project.oncpchange.connect(self.eventually_add_mp_to_menu) self.windows_menu.addSeparator() - self.window_layouts_menu = QMenu('Window layouts', self) - self.restore_layout_action = QAction('Restore default layout', self) + self.window_layouts_menu = QMenu("Window layouts", self) + self.restore_layout_action = QAction("Restore default layout", self) self.restore_layout_action.triggered.connect(self.setup_default_layout) self.window_layouts_menu.addAction(self.restore_layout_action) self.windows_menu.addMenu(self.window_layouts_menu) - self.panes_menu = QMenu('Panes', self) + self.panes_menu = QMenu("Panes", self) self.windows_menu.addMenu(self.panes_menu) - self.dataframe_menu = QMenu('DataFrame editors', self) + self.dataframe_menu = QMenu("DataFrame editors", self) self.dataframe_menu.addAction( - 'New Editor', partial(self.new_data_frame_editor, None, - 'DataFrame Editor')) + "New Editor", + partial(self.new_data_frame_editor, None, "DataFrame Editor"), + ) self.dataframe_menu.addSeparator() self.windows_menu.addMenu(self.dataframe_menu) - self.central_widgets_menu = menu = QMenu('Central widget', self) + self.central_widgets_menu = menu = QMenu("Central widget", self) self.windows_menu.addMenu(menu) self.central_widgets_actions = group = QActionGroup(self) group.setExclusive(True) @@ -469,8 +506,9 @@ class MainWindow(QMainWindow): # --------------------------------------------------------------------- self.console.help_explorer = help_explorer - psyp.default_print_func = partial(help_explorer.show_rst, - oname='formatoption_docs') + psyp.default_print_func = partial( + help_explorer.show_rst, oname="formatoption_docs" + ) psy.PlotterInterface._print_func = psyp.default_print_func self.setCentralWidget(self.console) @@ -493,18 +531,19 @@ class MainWindow(QMainWindow): # --------------------------------------------------------------------- # ------------------------- open_files_server ------------------------- # --------------------------------------------------------------------- - self.callbacks = {'new_plot': self.open_external.emit, - 'change_cwd': self._change_cwd, - 'run_script': self.console.run_script.emit, - 'command': self.console.run_command.emit, - } + self.callbacks = { + "new_plot": self.open_external.emit, + "change_cwd": self._change_cwd, + "run_script": self.console.run_script.emit, + "command": self.console.run_command.emit, + } # Server to open external files on a single instance - self.open_files_server = socket.socket(socket.AF_INET, - socket.SOCK_STREAM, - socket.IPPROTO_TCP) + self.open_files_server = socket.socket( + socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP + ) - if rcParams['main.listen_to_port']: + if rcParams["main.listen_to_port"]: self._file_thread = Thread(target=self.start_open_files_server) self._file_thread.setDaemon(True) self._file_thread.start() @@ -549,7 +588,7 @@ class MainWindow(QMainWindow): """Put focus on the ipython console""" self.console._control.setFocus() - def new_data_frame_editor(self, df=None, title='DataFrame Editor'): + def new_data_frame_editor(self, df=None, title="DataFrame Editor"): """Open a new dataframe editor Parameters @@ -565,8 +604,7 @@ class MainWindow(QMainWindow): The newly created editor""" editor = DataFrameEditor() self.dataframeeditors.append(editor) - editor.to_dock(self, title, - Qt.RightDockWidgetArea, docktype='df') + editor.to_dock(self, title, Qt.RightDockWidgetArea, docktype="df") if df is not None: editor.set_df(df) editor.show_plugin() @@ -576,13 +614,15 @@ class MainWindow(QMainWindow): def setup_default_layout(self): """Set up the default window layout""" - self.project_content.to_dock(self, 'Plot objects', - Qt.LeftDockWidgetArea) - self.ds_tree.to_dock(self, 'Datasets', Qt.LeftDockWidgetArea) - self.figures_tree.to_dock(self, 'Figures', Qt.LeftDockWidgetArea) - self.help_explorer.to_dock(self, 'Help explorer', - Qt.RightDockWidgetArea) - self.fmt_widget.to_dock(self, 'Formatoptions', Qt.BottomDockWidgetArea) + self.project_content.to_dock( + self, "Plot objects", Qt.LeftDockWidgetArea + ) + self.ds_tree.to_dock(self, "Datasets", Qt.LeftDockWidgetArea) + self.figures_tree.to_dock(self, "Figures", Qt.LeftDockWidgetArea) + self.help_explorer.to_dock( + self, "Help explorer", Qt.RightDockWidgetArea + ) + self.fmt_widget.to_dock(self, "Formatoptions", Qt.BottomDockWidgetArea) modify_widths = bool(self.default_widths) for w in map(self.plugins.__getitem__, self.default_plugins): @@ -590,8 +630,9 @@ class MainWindow(QMainWindow): w.show_plugin() if modify_widths and with_qt5: - self.resizeDocks([w.dock], [self.default_widths[w]], - Qt.Horizontal) + self.resizeDocks( + [w.dock], [self.default_widths[w]], Qt.Horizontal + ) # hide plugin widgets that should be hidden at startup for name, w in self.plugins.items(): @@ -615,6 +656,7 @@ class MainWindow(QMainWindow): name: str or QWidget The key or the plugin widget in the :attr:`plugins` dictionary""" from PyQt5.QtCore import QTimer + self.setUpdatesEnabled(False) current = self.centralWidget() if isinstance(name, six.string_types): @@ -623,7 +665,6 @@ class MainWindow(QMainWindow): new = name name = next(key for key, val in self.plugins.items() if val is new) if new is not current: - self._dock_widths = dock_widths = OrderedDict() self._dock_heights = dock_heights = OrderedDict() for key, w in self.plugins.items(): @@ -672,24 +713,25 @@ class MainWindow(QMainWindow): self.setUpdatesEnabled(True) def _save_project(self, p, new_fname=False, *args, **kwargs): - if new_fname or 'project_file' not in p.attrs: + if new_fname or "project_file" not in p.attrs: fname = QFileDialog.getSaveFileName( - self, 'Project destination', os.getcwd(), - 'Pickle files (*.pkl);;' - 'All files (*)' - ) + self, + "Project destination", + os.getcwd(), + "Pickle files (*.pkl);;" "All files (*)", + ) if with_qt5: # the filter is passed as well fname = fname[0] if not fname: return else: - fname = p.attrs['project_file'] + fname = p.attrs["project_file"] try: p.save_project(fname, *args, **kwargs) except Exception: - self.error_msg.showTraceback('<b>Could not save the project!</b>') + self.error_msg.showTraceback("<b>Could not save the project!</b>") else: - p.attrs['project_file'] = fname + p.attrs["project_file"] = fname if p.is_main: self.update_project_action(p.num) @@ -701,10 +743,11 @@ class MainWindow(QMainWindow): def _load_preset(self, project, *args, **kwargs): fname, ok = QFileDialog.getOpenFileName( - self, 'Load preset', os.path.join(get_configdir(), "presets"), - 'YAML files (*.yml *.yaml);;' - 'All files (*)' - ) + self, + "Load preset", + os.path.join(get_configdir(), "presets"), + "YAML files (*.yml *.yaml);;" "All files (*)", + ) if ok: project.load_preset(fname, *args, **kwargs) @@ -718,16 +761,17 @@ class MainWindow(QMainWindow): def _open_project(self, *args, **kwargs): fname = QFileDialog.getOpenFileName( - self, 'Project file', os.getcwd(), - 'Pickle files (*.pkl);;' - 'All files (*)' - ) + self, + "Project file", + os.getcwd(), + "Pickle files (*.pkl);;" "All files (*)", + ) if with_qt5: # the filter is passed as well fname = fname[0] if not fname: return p = psy.Project.load_project(fname, *args, **kwargs) - p.attrs['project_file'] = fname + p.attrs["project_file"] = fname self.update_project_action(p.num) def save_mp(self, *args, **kwargs): @@ -746,24 +790,27 @@ class MainWindow(QMainWindow): def _save_preset(self, project, *args, **kwargs): fname, ok = QFileDialog.getSaveFileName( - self, 'Save preset', os.path.join(get_configdir(), 'presets'), - 'YAML file (*.yml *.yaml);;' - 'All files (*)' - ) + self, + "Save preset", + os.path.join(get_configdir(), "presets"), + "YAML file (*.yml *.yaml);;" "All files (*)", + ) if ok: project.save_preset(fname, *args, **kwargs) def _export_project(self, p, *args, **kwargs): fname = QFileDialog.getSaveFileName( - self, 'Picture destination', os.getcwd(), - 'PDF files (*.pdf);;' - 'Postscript file (*.ps);;' - 'PNG image (*.png);;' - 'JPG image (*.jpg *.jpeg);;' - 'TIFF image (*.tif *.tiff);;' - 'GIF image (*.gif);;' - 'All files (*)' - ) + self, + "Picture destination", + os.getcwd(), + "PDF files (*.pdf);;" + "Postscript file (*.ps);;" + "PNG image (*.png);;" + "JPG image (*.jpg *.jpeg);;" + "TIFF image (*.tif *.tiff);;" + "GIF image (*.gif);;" + "All files (*)", + ) if with_qt5: # the filter is passed as well fname = fname[0] if not fname: @@ -772,7 +819,8 @@ class MainWindow(QMainWindow): p.export(fname, *args, **kwargs) except Exception: self.error_msg.showTraceback( - '<b>Could not export the figures!</b>') + "<b>Could not export the figures!</b>" + ) def export_mp(self, *args, **kwargs): self._export_project(psy.gcp(True), **kwargs) @@ -781,14 +829,15 @@ class MainWindow(QMainWindow): self._export_project(psy.gcp(), **kwargs) def new_plots(self, exec_=None): - if hasattr(self, 'plot_creator'): + if hasattr(self, "plot_creator"): try: self.plot_creator.close() except RuntimeError: pass self.plot_creator = PlotCreator( - help_explorer=self.help_explorer, parent=self) - available_width = QDesktopWidget().availableGeometry().width() / 3. + help_explorer=self.help_explorer, parent=self + ) + available_width = QDesktopWidget().availableGeometry().width() // 3 width = self.plot_creator.sizeHint().width() height = self.plot_creator.sizeHint().height() # The plot creator window should cover at least one third of the screen @@ -802,7 +851,7 @@ class MainWindow(QMainWindow): def edit_preferences(self, exec_=None): """Edit Spyder preferences""" - if hasattr(self, 'preferences'): + if hasattr(self, "preferences"): try: self.preferences.close() except RuntimeError: @@ -813,7 +862,7 @@ class MainWindow(QMainWindow): widget.initialize() dlg.add_page(widget) available_width = int( - 0.667*QDesktopWidget().availableGeometry().width() + 0.667 * QDesktopWidget().availableGeometry().width() ) width = dlg.sizeHint().width() height = dlg.sizeHint().height() @@ -825,21 +874,22 @@ class MainWindow(QMainWindow): def about(self): """About the tool""" versions = { - key: d['version'] for key, d in psyplot.get_versions(False).items() - } - versions.update(psyplot_gui.get_versions()['requirements']) - versions.update(psyplot._get_versions()['requirements']) - versions['github'] = 'https://github.com/psyplot/psyplot' - versions['author'] = psyplot.__author__ + key: d["version"] for key, d in psyplot.get_versions(False).items() + } + versions.update(psyplot_gui.get_versions()["requirements"]) + versions.update(psyplot._get_versions()["requirements"]) + versions["github"] = "https://github.com/psyplot/psyplot" + versions["author"] = psyplot.__author__ QMessageBox.about( - self, "About psyplot", - u"""<b>psyplot: Interactive data visualization with python</b> + self, + "About psyplot", + """<b>psyplot: Interactive data visualization with python</b> <br>Copyright © 2017- Philipp Sommer <br>Licensed under the terms of the GNU General Public License v2 (GPLv2) <p>Created by %(author)s</p> <p>Most of the icons come from the - <a href="https://www.iconfinder.com/"> iconfinder</a>.</p> + <a href="https://fontawesome.com/">Fontawesom</a>.</p> <p>For bug reports and feature requests, please go to our <a href="%(github)s">Github website</a> or contact the author via mail.</p> @@ -859,17 +909,19 @@ class MainWindow(QMainWindow): in the <em>Help</em> menu.</p> <p>This software is provided "as is", without warranty or support of any kind.</p>""" - % versions) + % versions, + ) def show_dependencies(self, exec_=None): """Open a dialog that shows the dependencies""" - if hasattr(self, 'dependencies'): + if hasattr(self, "dependencies"): try: self.dependencies.close() except RuntimeError: pass - self.dependencies = dlg = DependenciesDialog(psyplot.get_versions(), - parent=self) + self.dependencies = dlg = DependenciesDialog( + psyplot.get_versions(), parent=self + ) dlg.resize(630, 420) if exec_: dlg.exec_() @@ -880,10 +932,13 @@ class MainWindow(QMainWindow): def add_mp_to_menu(self): mp = psy.gcp(True) - action = QAction(os.path.basename(mp.attrs.get( - 'project_file', 'Untitled %s*' % mp.num)), self) - action.setStatusTip( - 'Make project %s the current project' % mp.num) + action = QAction( + os.path.basename( + mp.attrs.get("project_file", "Untitled %s*" % mp.num) + ), + self, + ) + action.setStatusTip("Make project %s the current project" % mp.num) action.triggered.connect(lambda: psy.scp(psy.project(mp.num))) self.project_actions[mp.num] = action self.windows_menu.addAction(action) @@ -892,12 +947,16 @@ class MainWindow(QMainWindow): action = self.project_actions.get(num) p = psy.project(num) if action: - action.setText(os.path.basename(p.attrs.get( - 'project_file', 'Untitled %s*' % num))) + action.setText( + os.path.basename( + p.attrs.get("project_file", "Untitled %s*" % num) + ) + ) def eventually_add_mp_to_menu(self, p): for num in set(self.project_actions).difference( - psy.get_project_nums()): + psy.get_project_nums() + ): self.windows_menu.removeAction(self.project_actions.pop(num)) if p is None or not p.is_main: return @@ -909,11 +968,12 @@ class MainWindow(QMainWindow): creator for new files This method is inspired and to most parts copied from spyder""" - self.open_files_server.setsockopt(socket.SOL_SOCKET, - socket.SO_REUSEADDR, 1) - port = rcParams['main.open_files_port'] + self.open_files_server.setsockopt( + socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 + ) + port = rcParams["main.open_files_port"] try: - self.open_files_server.bind(('127.0.0.1', port)) + self.open_files_server.bind(("127.0.0.1", port)) except Exception: return self.open_files_server.listen(20) @@ -928,21 +988,23 @@ class MainWindow(QMainWindow): if e.args[0] == eintr: continue # handle a connection abort on close error - enotsock = (errno.WSAENOTSOCK if os.name == 'nt' - else errno.ENOTSOCK) + enotsock = ( + errno.WSAENOTSOCK if os.name == "nt" else errno.ENOTSOCK + ) if e.args[0] in [errno.ECONNABORTED, enotsock]: return raise args = pickle.loads(req.recv(1024)) callback = args[0] func = self.callbacks[callback] - self.logger.debug('Emitting %s callback %s', callback, func) + self.logger.debug("Emitting %s callback %s", callback, func) func(args[1:]) - req.sendall(b' ') + req.sendall(b" ") def change_cwd(self, path): """Change the current working directory""" import os + os.chdir(path) def _change_cwd(self, args): @@ -950,29 +1012,54 @@ class MainWindow(QMainWindow): self.change_cwd(path) docstrings.keep_params( - 'make_plot.parameters', 'fnames', 'project', 'engine', 'plot_method', - 'name', 'dims', 'encoding', 'enable_post', 'seaborn_style', - 'concat_dim', 'chname', 'preset') + "make_plot.parameters", + "fnames", + "project", + "engine", + "plot_method", + "name", + "dims", + "encoding", + "enable_post", + "seaborn_style", + "concat_dim", + "chname", + "preset", + ) def open_files(self, fnames): """Open a file and ask the user how""" - fnames_s = ', '.join(map(os.path.basename, fnames)) + fnames_s = ", ".join(map(os.path.basename, fnames)) if len(fnames_s) > 30: - fnames_s = fnames_s[:27] + '...' + fnames_s = fnames_s[:27] + "..." item, ok = QInputDialog.getItem( - self, 'Open file...', 'Open %s as...' % fnames_s, - list(self.open_file_options), current=0, editable=False) + self, + "Open file...", + "Open %s as..." % fnames_s, + list(self.open_file_options), + current=0, + editable=False, + ) if ok: return self.open_file_options[item](fnames) - @docstrings.get_sections(base='MainWindow.open_external_files') + @docstrings.get_sections(base="MainWindow.open_external_files") @docstrings.dedent - def open_external_files(self, fnames=[], project=None, engine=None, - plot_method=None, name=None, dims=None, - encoding=None, enable_post=False, - seaborn_style=None, concat_dim=get_default_value( - xr.open_mfdataset, 'concat_dim'), chname={}, - preset=None): + def open_external_files( + self, + fnames=[], + project=None, + engine=None, + plot_method=None, + name=None, + dims=None, + encoding=None, + enable_post=False, + seaborn_style=None, + concat_dim=get_default_value(xr.open_mfdataset, "concat_dim"), + chname={}, + preset=None, + ): """ Open external files @@ -982,51 +1069,64 @@ class MainWindow(QMainWindow): """ if seaborn_style is not None: import seaborn as sns + sns.set_style(seaborn_style) if project is not None: - fnames = [s.split(',') for s in fnames] + fnames = [s.split(",") for s in fnames] if not isinstance(project, dict): project = psyd.safe_list(project)[0] - single_files = (l[0] for l in fnames if len(l) == 1) + single_files = (files[0] for files in fnames if len(files) == 1) alternative_paths = defaultdict(lambda: next(single_files, None)) - alternative_paths.update(list(l for l in fnames if len(l) == 2)) + alternative_paths.update( + list(files for files in fnames if len(files) == 2) + ) p = psy.Project.load_project( - project, alternative_paths=alternative_paths, - engine=engine, main=not psy.gcp(), encoding=encoding, - enable_post=enable_post, chname=chname) + project, + alternative_paths=alternative_paths, + engine=engine, + main=not psy.gcp(), + encoding=encoding, + enable_post=enable_post, + chname=chname, + ) if preset: p.load_preset(preset) if isinstance(project, six.string_types): - p.attrs.setdefault('project_file', project) + p.attrs.setdefault("project_file", project) return True else: self.new_plots(False) - self.plot_creator.open_dataset(fnames, engine=engine, - concat_dim=concat_dim) - if name == 'all': + self.plot_creator.open_dataset( + fnames, engine=engine, concat_dim=concat_dim + ) + if name == "all": ds = self.plot_creator.get_ds() name = sorted(set(ds.variables) - set(ds.coords)) self.plot_creator.insert_array( - list(filter(None, psy.safe_list(name)))) + list(filter(None, psy.safe_list(name))) + ) if dims is not None: ds = self.plot_creator.get_ds() - dims = {key: ', '.join( - map(str, val)) for key, val in six.iteritems( - dims)} + dims = { + key: ", ".join(map(str, val)) + for key, val in six.iteritems(dims) + } for i, vname in enumerate( - self.plot_creator.array_table.vnames): + self.plot_creator.array_table.vnames + ): self.plot_creator.array_table.selectRow(i) - self.plot_creator.array_table.update_selected( - ) + self.plot_creator.array_table.update_selected() self.plot_creator.array_table.selectAll() var = ds[vname[0]] self.plot_creator.array_table.update_selected( - dims=var.psy.decoder.correct_dims(var, dims.copy())) + dims=var.psy.decoder.correct_dims(var, dims.copy()) + ) if preset: self.plot_creator.set_preset(preset) if plot_method: self.plot_creator.pm_combo.setCurrentIndex( - self.plot_creator.pm_combo.findText(plot_method)) + self.plot_creator.pm_combo.findText(plot_method) + ) self.plot_creator.exec_() return True @@ -1034,13 +1134,24 @@ class MainWindow(QMainWindow): self.open_external_files(*args) @classmethod - @docstrings.get_sections(base='MainWindow.run') + @docstrings.get_sections(base="MainWindow.run") @docstrings.dedent - def run(cls, fnames=[], project=None, engine=None, plot_method=None, - name=None, dims=None, encoding=None, enable_post=False, - seaborn_style=None, - concat_dim=get_default_value(xr.open_mfdataset, 'concat_dim'), - chname={}, preset=None, show=True): + def run( + cls, + fnames=[], + project=None, + engine=None, + plot_method=None, + name=None, + dims=None, + encoding=None, + enable_post=False, + seaborn_style=None, + concat_dim=get_default_value(xr.open_mfdataset, "concat_dim"), + chname={}, + preset=None, + show=True, + ): """ Create a mainwindow and open the given files or project @@ -1066,13 +1177,25 @@ class MainWindow(QMainWindow): _set_mainwindow(mainwindow) if fnames or project: mainwindow.open_external_files( - fnames, project, engine, plot_method, name, dims, encoding, - enable_post, seaborn_style, concat_dim, chname, preset) + fnames, + project, + engine, + plot_method, + name, + dims, + encoding, + enable_post, + seaborn_style, + concat_dim, + chname, + preset, + ) psyplot.with_gui = True return mainwindow - def register_shortcut(self, action, shortcut, - context=Qt.ApplicationShortcut): + def register_shortcut( + self, action, shortcut, context=Qt.ApplicationShortcut + ): """Register an action for a shortcut""" shortcuts = psy.safe_list(shortcut) for j, shortcut in enumerate(shortcuts): @@ -1080,8 +1203,10 @@ class MainWindow(QMainWindow): for i, (s, a) in enumerate(self.current_shortcuts): if s == shortcut: new_shortcuts = [ - sc for sc in self.current_shortcuts[i][1].shortcuts() - if sc != s] + sc + for sc in self.current_shortcuts[i][1].shortcuts() + if sc != s + ] a.setShortcut(QKeySequence()) if new_shortcuts: a.setShortcuts(new_shortcuts) diff --git a/psyplot_gui/plot_creator.py b/psyplot_gui/plot_creator.py index 5f62875dfd6d10eff3139a9f9e5921dbf5ada8cc..6cfa3afb13250d88679fa76e3836c13c313d25ac 100644 --- a/psyplot_gui/plot_creator.py +++ b/psyplot_gui/plot_creator.py @@ -4,57 +4,74 @@ The main class is the :class:`PlotCreator` which is used to handle the different plotting methods of the :class:`psyplot.project.ProjectPlotter` class""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only from __future__ import division -import os + import logging +import os import re import types -import xarray -from functools import partial -import numpy as np from collections import defaultdict +from functools import partial +from itertools import chain, cycle, product, repeat, starmap from math import floor -from itertools import chain, product, cycle, repeat, starmap + import matplotlib as mpl +import numpy as np +import psyplot.project as psy import six +import xarray +from psyplot.config.rcsetup import get_configdir from psyplot.utils import _temp_bool_prop -from psyplot.compat.pycompat import map, range, filter, OrderedDict + +from psyplot_gui.common import ( + ListValidator, + LoadFromConsoleButton, + PyErrorMessage, + get_icon, +) from psyplot_gui.compat.qtcompat import ( - QWidget, QComboBox, QHBoxLayout, QVBoxLayout, QFileDialog, QToolButton, - QIcon, Qt, QListView, QtCore, with_qt5, QAbstractItemView, QPushButton, - QLabel, QValidator, QStyledItemDelegate, QLineEdit, QCheckBox, isstring, - QTableWidget, QTableWidgetItem, QGridLayout, QIntValidator, QMenu, QAction, - QInputDialog, QTabWidget, QDoubleValidator, QGraphicsScene, asstring, - QGraphicsRectItem, QGraphicsView, QDialog, QDialogButtonBox, QSplitter) -from psyplot_gui.common import (get_icon, ListValidator, PyErrorMessage, - LoadFromConsoleButton) + QAbstractItemView, + QAction, + QCheckBox, + QComboBox, + QDialog, + QDialogButtonBox, + QDoubleValidator, + QFileDialog, + QGraphicsRectItem, + QGraphicsScene, + QGraphicsView, + QGridLayout, + QHBoxLayout, + QIcon, + QIntValidator, + QLabel, + QLineEdit, + QListView, + QMenu, + QPushButton, + QSplitter, + QStyledItemDelegate, + Qt, + QTableWidget, + QTableWidgetItem, + QTabWidget, + QtCore, + QToolButton, + QValidator, + QVBoxLayout, + QWidget, + asstring, + isstring, + with_qt5, +) from psyplot_gui.preferences import RcParamsTree -import psyplot.project as psy -from psyplot.config.rcsetup import get_configdir - logger = logging.getLogger(__name__) @@ -67,8 +84,8 @@ class CoordComboBox(QComboBox): emitted when the popup is about to be closed because the user clicked on a value""" - close_popups = _temp_bool_prop('close_popups', default=True) - use_coords = _temp_bool_prop('use_coords', default=False) + close_popups = _temp_bool_prop("close_popups", default=True) + use_coords = _temp_bool_prop("use_coords", default=False) leftclick = QtCore.pyqtSignal(QComboBox) def __init__(self, ds_func, dim, parent=None): @@ -124,8 +141,9 @@ class CoordComboBox(QComboBox): def eventFilter(self, obj, event): """Reimplemented to filter right-click events on the view()""" - ret = ((event.type() == QtCore.QEvent.MouseButtonPress) and - event.button() == Qt.RightButton) + ret = ( + event.type() == QtCore.QEvent.MouseButtonPress + ) and event.button() == Qt.RightButton return ret def handleItemPressed(self, index): @@ -147,8 +165,7 @@ class CoordComboBox(QComboBox): self._changed = True def hide_anyway(self, index=None): - """Function to hide the popup despite of the :attr:`_changed` attribute - """ + """Function to hide the popup despite of the :attr:`_changed` attribute""" self._changed = True self.hidePopup() @@ -177,7 +194,7 @@ class CoordComboBox(QComboBox): it (if it is empty)""" if self._is_empty: ds = self.get_ds() - self.addItem('') + self.addItem("") if self.use_coords: self.addItems(ds[self.dim].astype(str).values) else: @@ -199,7 +216,7 @@ class ArrayNameValidator(QValidator): s = asstring(s) if not s: return self.table.next_available_name() - return self.table.next_available_name(s + '_{0}') + return self.table.next_available_name(s + "_{0}") def validate(self, s, pos): s = asstring(s) @@ -220,8 +237,9 @@ class ArrayNameItemDelegate(QStyledItemDelegate): return editor = QLineEdit(widget) item = self.parent().item(index.row(), index.column()) - validator = ArrayNameValidator(item.text() if item else '', - self.parent(), editor) + validator = ArrayNameValidator( + item.text() if item else "", self.parent(), editor + ) editor.setValidator(validator) return editor @@ -237,8 +255,9 @@ class VariableItemDelegate(QStyledItemDelegate): return editor = QLineEdit(widget) ds = self.parent().get_ds() - validator = ListValidator(ds.variables.keys(), self.parent().sep, - editor) + validator = ListValidator( + ds.variables.keys(), self.parent().sep, editor + ) editor.setValidator(validator) return editor @@ -253,13 +272,22 @@ class VariablesTable(QTableWidget): def selected_variables(self): """The currently selected variables""" return [ - self.variables[i] for i in map( + self.variables[i] + for i in map( list(map(asstring, self.variables)).index, - sorted(set(item.text() for item in self.selectedItems() - if item.column() == 0)))] - - def __init__(self, get_func, columns=['long_name', 'dims', 'shape'], - *args, **kwargs): + sorted( + set( + item.text() + for item in self.selectedItems() + if item.column() == 0 + ) + ), + ) + ] + + def __init__( + self, get_func, columns=["long_name", "dims", "shape"], *args, **kwargs + ): """ Parameters ---------- @@ -281,7 +309,7 @@ class VariablesTable(QTableWidget): else: self.column_labels = columns self.setColumnCount(len(columns) + 1) - self.setHorizontalHeaderLabels(['variable'] + columns) + self.setHorizontalHeaderLabels(["variable"] + columns) def fill_from_ds(self, ds=None): """Clear the table and insert items from the given `dataset`""" @@ -298,13 +326,22 @@ class VariablesTable(QTableWidget): variable = ds.variables[vname] self.setItem(i, 0, QTableWidgetItem(asstring(vname))) for j, attr in enumerate(self.column_labels, 1): - if attr == 'dims': - self.setItem(i, j, QTableWidgetItem( - ', '.join(variable.dims))) + if attr == "dims": + self.setItem( + i, j, QTableWidgetItem(", ".join(variable.dims)) + ) else: - self.setItem(i, j, QTableWidgetItem( - str(variable.attrs.get(attr, getattr( - variable, attr, ''))))) + self.setItem( + i, + j, + QTableWidgetItem( + str( + variable.attrs.get( + attr, getattr(variable, attr, "") + ) + ) + ), + ) class CoordsTable(QTableWidget): @@ -331,9 +368,12 @@ class CoordsTable(QTableWidget): @property def combo_boxes(self): """A list of :class:`CoordComboBox` in this table""" - return list(filter( - lambda w: w is not None, - (self.cellWidget(0, i) for i in range(self.columnCount())))) + return list( + filter( + lambda w: w is not None, + (self.cellWidget(0, i) for i in range(self.columnCount())), + ) + ) def fill_from_ds(self, ds=None): """Clear the table and create new comboboxes""" @@ -346,8 +386,9 @@ class CoordsTable(QTableWidget): return coords = list(ds.coords) vnames = [v for v in ds.variables if v not in coords] - self.dims = dims = list(set( - chain(*(ds.variables[vname].dims for vname in vnames)))) + self.dims = dims = list( + set(chain(*(ds.variables[vname].dims for vname in vnames))) + ) try: dims.sort() except TypeError: @@ -363,7 +404,8 @@ class CoordsTable(QTableWidget): first row""" return QtCore.QSize( super(CoordsTable, self).sizeHint().width(), - self.horizontalHeader().height() + self.rowHeight(0)) + self.horizontalHeader().height() + self.rowHeight(0), + ) class DragDropTable(QTableWidget): @@ -388,8 +430,9 @@ class DragDropTable(QTableWidget): def dropEvent(self, event): if event.source() == self and ( - event.dropAction() == Qt.MoveAction or - self.dragDropMode() == QAbstractItemView.InternalMove): + event.dropAction() == Qt.MoveAction + or self.dragDropMode() == QAbstractItemView.InternalMove + ): self.dropOn(event) else: @@ -434,9 +477,11 @@ class DragDropTable(QTableWidget): if self.dragDropMode() == QAbstractItemView.InternalMove: dropAction = Qt.MoveAction - if (event.source() == self and - event.possibleActions() & Qt.MoveAction and - dropAction == Qt.MoveAction): + if ( + event.source() == self + and event.possibleActions() & Qt.MoveAction + and dropAction == Qt.MoveAction + ): selectedIndexes = self.selectedIndexes() child = index while child.isValid() and child != self.rootIndex(): @@ -456,13 +501,15 @@ class DragDropTable(QTableWidget): if self.viewport().rect().contains(event.pos()): index = self.indexAt(event.pos()) if not index.isValid() or not self.visualRect(index).contains( - event.pos()): + event.pos() + ): index = self.rootIndex() if self.model().supportedDropActions() & event.dropAction(): if index != self.rootIndex(): dropIndicatorPosition = self.position( - event.pos(), self.visualRect(index), index) + event.pos(), self.visualRect(index), index + ) if dropIndicatorPosition == QAbstractItemView.AboveItem: row = index.row() @@ -488,7 +535,8 @@ class DragDropTable(QTableWidget): r = QAbstractItemView.OnItem if r == QAbstractItemView.OnItem and not ( - self.model().flags(index) & Qt.ItemIsDropEnabled): + self.model().flags(index) & Qt.ItemIsDropEnabled + ): if pos.y() < rect.center().y(): r = QAbstractItemView.AboveItem else: @@ -517,40 +565,45 @@ class ArrayTable(DragDropTable): 5. Columns containing the dimension informations""" #: Pattern to interprete subplots - subplot_patt = re.compile(r'\((?P<fig>\d+),\s*' # figure - r'(?P<rows>\d+),\s*' # rows - r'(?P<cols>\d+),\s*' # columns - r'(?P<num1>\d+),\s*' # position - r'(?P<num2>\d+)\s*\)' # end subplot - ) + subplot_patt = re.compile( + r"\((?P<fig>\d+),\s*" # figure + r"(?P<rows>\d+),\s*" # rows + r"(?P<cols>\d+),\s*" # columns + r"(?P<num1>\d+),\s*" # position + r"(?P<num2>\d+)\s*\)" # end subplot + ) #: pattern to interprete arbitrary axes - axes_patt = re.compile(r'\((?P<fig>\d+),\s*' # figure - r'(?P<x0>0*\.\d+),\s*' # lower left x - r'(?P<y0>0*\.\d+),\s*' # lower left y - r'(?P<x1>0*\.\d+),\s*' # upper right x - r'(?P<y1>0*\.\d+)\s*\)' # upper right y - ) + axes_patt = re.compile( + r"\((?P<fig>\d+),\s*" # figure + r"(?P<x0>0*\.\d+),\s*" # lower left x + r"(?P<y0>0*\.\d+),\s*" # lower left y + r"(?P<x1>0*\.\d+),\s*" # upper right x + r"(?P<y1>0*\.\d+)\s*\)" # upper right y + ) #: The separator for variable names - sep = ';;' + sep = ";;" #: Tool tip for the variable column - VARIABLE_TT = ("The variables of the array from the dataset. Multiple" - "variables for one array may be separated by '%s'" % ( - sep)) + VARIABLE_TT = ( + "The variables of the array from the dataset. Multiple" + "variables for one array may be separated by '%s'" % (sep) + ) #: Base tool tip for a dimension column - DIMS_TT = ("The values for dimension %s." - " You can use integers either explicit, e.g." - "<ul>" - "<li>1, 2, 3, ...,</li>" - "</ul>" - "or slices like <em>start:end:step</em>, e.g." - "<ul>" - "<li>'1:6:2'</li>" - "</ul>" - "where the latter is equivalent to '1, 3, 5'") + DIMS_TT = ( + "The values for dimension %s." + " You can use integers either explicit, e.g." + "<ul>" + "<li>1, 2, 3, ...,</li>" + "</ul>" + "or slices like <em>start:end:step</em>, e.g." + "<ul>" + "<li>'1:6:2'</li>" + "</ul>" + "where the latter is equivalent to '1, 3, 5'" + ) def dropEvent(self, event): """Reimplemented to call the :meth:`check_arrays` after the call""" @@ -560,10 +613,14 @@ class ArrayTable(DragDropTable): # sure that those arrays are not considered when checking for # duplicates messages = dict( - zip(self.current_names, [msg for b, msg in self.check_arrays()])) + zip(self.current_names, [msg for b, msg in self.check_arrays()]) + ) super(ArrayTable, self).dropEvent(event) - ignores = [arr_name for arr_name, msg in messages.items() - if not msg.startswith('Found duplicated entry of')] + ignores = [ + arr_name + for arr_name, msg in messages.items() + if not msg.startswith("Found duplicated entry of") + ] self.check_arrays(ignore_duplicates=ignores) @property @@ -577,32 +634,39 @@ class ArrayTable(DragDropTable): if self.prefer_list: return [] arr_col = self.arr_col - return [asstring(item.text()) for item in filter(None, map( - lambda i: self.item(i, arr_col), range(self.rowCount())))] + return [ + asstring(item.text()) + for item in filter( + None, + map(lambda i: self.item(i, arr_col), range(self.rowCount())), + ) + ] @property def vnames(self): """The list of variable names per array""" var_col = self.var_col - return [self.item(i, var_col).text().split(';;') - for i in range(self.rowCount())] + return [ + self.item(i, var_col).text().split(";;") + for i in range(self.rowCount()) + ] @property def arr_names_dict(self): """The final dictionary containing the array names necessary for the `arr_names` parameter in the - :meth:`psyplot.data.ArrayList.from_dataset` method """ - ret = OrderedDict() + :meth:`psyplot.data.ArrayList.from_dataset` method""" + ret = dict() arr_col = self.arr_col for irow in range(self.rowCount()): arr_name = asstring(self.item(irow, arr_col).text()) if self.plot_method and self.plot_method._prefer_list: d = ret.setdefault(arr_name, defaultdict(list)) - d['name'].append(self._get_variables(irow)) + d["name"].append(self._get_variables(irow)) for key, val in self._get_dims(irow).items(): d[key].append(val) else: - ret[arr_name] = d = {'name': self._get_variables(irow)} + ret[arr_name] = d = {"name": self._get_variables(irow)} d.update(self._get_dims(irow)) return ret @@ -621,16 +685,16 @@ class ArrayTable(DragDropTable): if pm is not None: projection = self.plot_method.plotter_cls._get_sample_projection() if projection is not None: - kwargs['projection'] = projection + kwargs["projection"] = projection for irow in range(self.rowCount()): arr_name = self.item(irow, arr_col).text() if arr_name in d: continue d.add(arr_name) axes_type, args = self.axes_info(self.item(irow, axes_col)) - if axes_type == 'subplot': + if axes_type == "subplot": ret.append(SubplotCreator.create_subplot(*args, **kwargs)) - elif axes_type == 'axes': + elif axes_type == "axes": ret.append(AxesCreator.create_axes(*args, **kwargs)) else: ret.append(None) @@ -667,12 +731,16 @@ class ArrayTable(DragDropTable): The coordinates in the dataset""" super(ArrayTable, self).__init__(*args, **kwargs) self.get_ds = get_func - self.VARIABLE_LABEL = 'variable' - self.ARRAY_LABEL = 'array name' - self.AXES_LABEL = 'axes' - self.CHECK_LABEL = 'check' - self.desc_cols = [self.VARIABLE_LABEL, self.ARRAY_LABEL, - self.AXES_LABEL, self.CHECK_LABEL] + self.VARIABLE_LABEL = "variable" + self.ARRAY_LABEL = "array name" + self.AXES_LABEL = "axes" + self.CHECK_LABEL = "check" + self.desc_cols = [ + self.VARIABLE_LABEL, + self.ARRAY_LABEL, + self.AXES_LABEL, + self.CHECK_LABEL, + ] self.plot_method = None self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setContextMenuPolicy(Qt.CustomContextMenu) @@ -680,7 +748,8 @@ class ArrayTable(DragDropTable): self.set_columns(columns) self.setItemDelegateForColumn(self.var_col, VariableItemDelegate(self)) self.setItemDelegateForColumn( - self.arr_col, ArrayNameItemDelegate(self)) + self.arr_col, ArrayNameItemDelegate(self) + ) self.itemChanged.connect(self.check_item) self.itemChanged.connect(self.update_other_items) @@ -723,7 +792,8 @@ class ArrayTable(DragDropTable): coords = list(ds.coords) vnames = [v for v in ds.variables if v not in coords] self.dims = dims = list( - set(chain(*(ds.variables[vname].dims for vname in vnames)))) + set(chain(*(ds.variables[vname].dims for vname in vnames))) + ) try: dims.sort() except TypeError: @@ -747,7 +817,7 @@ class ArrayTable(DragDropTable): self.setRowCount(irow + 1) self.setItem(irow, 0, QTableWidgetItem(asstring(name))) self.setItem(irow, 1, QTableWidgetItem(self.next_available_name())) - self.setItem(irow, 2, QTableWidgetItem('')) + self.setItem(irow, 2, QTableWidgetItem("")) for dim in dims.intersection(kwargs): icol = len(self.desc_cols) + self.dims.index(dim) self.setItem(irow, icol, QTableWidgetItem(kwargs[dim])) @@ -783,15 +853,18 @@ class ArrayTable(DragDropTable): irows = {item.row() for item in self.selectedItems()} var_col = self.desc_cols.index(self.VARIABLE_LABEL) for irow in irows: - vname = asstring( - self.item(irow, var_col).text()).split(self.sep)[0].strip() + vname = ( + asstring(self.item(irow, var_col).text()) + .split(self.sep)[0] + .strip() + ) var_dims = set(ds.variables[vname].dims) for dim in var_dims.intersection(dims): icol = len(self.desc_cols) + self.dims.index(dim) item = self.item(irow, icol) curr_text = asstring(item.text()) if curr_text: - curr_text += ', ' + curr_text += ", " item.setText(curr_text + dims[dim]) if check: for irow in irows: @@ -800,11 +873,13 @@ class ArrayTable(DragDropTable): def add_subplots(self, rows, cols, maxn=None): """Add multiple subplots to the selected arrays""" import matplotlib.pyplot as plt + irows = sorted({ind.row() for ind in self.selectedIndexes()}) irows = irows or list(range(self.rowCount())) maxn = maxn or rows * cols - figs = chain(*( - [i] * maxn for i in range(1, 1000) if i not in plt.get_fignums())) + figs = chain( + *([i] * maxn for i in range(1, 1000) if i not in plt.get_fignums()) + ) nums = cycle(range(1, maxn + 1)) seen = set() axes_col = self.desc_cols.index(self.AXES_LABEL) @@ -815,14 +890,14 @@ class ArrayTable(DragDropTable): continue seen.add(arr_item.text()) num = next(nums) - text = '(%i, %i, %i, %i, %i)' % ( - next(figs), rows, cols, num, num) + text = "(%i, %i, %i, %i, %i)" % (next(figs), rows, cols, num, num) item = QTableWidgetItem(text) self.setItem(irow, axes_col, item) def add_single_subplot(self, rows, cols, row, col): """Add one subplot to the selected arrays on multiple figures""" import matplotlib.pyplot as plt + irows = sorted({ind.row() for ind in self.selectedIndexes()}) irows = irows or list(range(self.rowCount())) figs = (num for num in range(1, 1000) if num not in plt.get_fignums()) @@ -835,8 +910,7 @@ class ArrayTable(DragDropTable): if arr_item is None or arr_item.text() in seen: continue seen.add(arr_item.text()) - text = '(%i, %i, %i, %i, %i)' % ( - next(figs), rows, cols, num, num) + text = "(%i, %i, %i, %i, %i)" % (next(figs), rows, cols, num, num) item = QTableWidgetItem(text) self.setItem(irow, axes_col, item) @@ -854,15 +928,16 @@ class ArrayTable(DragDropTable): rows""" axes_col = self.desc_cols.index(self.AXES_LABEL) items = [self.item(row, axes_col) for row in rows] - action = QAction('Select subplot', self) + action = QAction("Select subplot", self) types_and_args = list( - filter(lambda t: t[0], map(self.axes_info, items))) + filter(lambda t: t[0], map(self.axes_info, items)) + ) types = [t[0] for t in types_and_args] if types and all(t == types[0] for t in types): - if types[0] == 'subplot': - creator_kws = ['fig', 'rows', 'cols', 'num1', 'num2'] - elif types[0] == 'axes': - creator_kws = ['fig', 'x0', 'y0', 'x1', 'y1'] + if types[0] == "subplot": + creator_kws = ["fig", "rows", "cols", "num1", "num2"] + elif types[0] == "axes": + creator_kws = ["fig", "x0", "y0", "x1", "y1"] else: creator_kws = [] func_name = types[0] @@ -872,7 +947,7 @@ class ArrayTable(DragDropTable): kwargs = {} if len(items) > 0: - kwargs['fig'] = '' + kwargs["fig"] = "" for kw, vals in zip(creator_kws, zip(*args)): if all(val == vals[0] for val in vals): @@ -882,7 +957,8 @@ class ArrayTable(DragDropTable): kwargs = {} action.triggered.connect( - self._open_axes_creator(items, func_name, kwargs)) + self._open_axes_creator(items, func_name, kwargs) + ) return action def _change_axes(self, items, iterator): @@ -896,14 +972,15 @@ class ArrayTable(DragDropTable): item.setText(text) def _open_axes_creator(self, items, func_name, kwargs): - def func(): - if hasattr(self, '_axes_creator'): + if hasattr(self, "_axes_creator"): self._axes_creator.close() self._axes_creator = obj = AxesCreatorCollection( - func_name, kwargs, parent=self) + func_name, kwargs, parent=self + ) obj.okpressed.connect(partial(self._change_axes, items)) obj.exec_() + return func def axes_info(self, s): @@ -911,11 +988,12 @@ class ArrayTable(DragDropTable): s = asstring(s) if isstring(s) else asstring(s.text()) m = self.subplot_patt.match(s) if m: - return 'subplot', list(map(int, m.groups())) + return "subplot", list(map(int, m.groups())) m = self.axes_patt.match(s) if m: - return 'axes', [int(m.groupdict()['fig'])] + list(map( - float, m.groups()[1:])) + return "axes", [int(m.groupdict()["fig"])] + list( + map(float, m.groups()[1:]) + ) return None, None def set_pm(self, s): @@ -930,7 +1008,8 @@ class ArrayTable(DragDropTable): return for irow in range(self.rowCount()): other_item = self.item( - irow, self.desc_cols.index(self.ARRAY_LABEL)) + irow, self.desc_cols.index(self.ARRAY_LABEL) + ) if other_item is not None: self.check_array(irow) @@ -956,8 +1035,8 @@ class ArrayTable(DragDropTable): self.blockSignals(False) def get_all_rows(self, row): - """Return all the rows that have the same array name as the given `row` - """ + """Return all the rows that have the same array name as the given `row`""" + def check_item(row): item = self.item(row, arr_col) return item is not None and item.text() == arr_name @@ -971,23 +1050,24 @@ class ArrayTable(DragDropTable): def check_array(self, row, ignore_duplicates=[]): """check whether the array variables are valid, the array name is valid, the axes info is valid and the dimensions""" + def set_check(row, valid, msg): check_item = QTableWidgetItem() check_item.setFlags(check_item.flags() ^ Qt.ItemIsEditable) if valid: - check_item.setIcon(QIcon(get_icon('valid.png'))) + check_item.setIcon(QIcon(get_icon("valid.png"))) elif valid is None: - check_item.setIcon(QIcon(get_icon('warning.png'))) + check_item.setIcon(QIcon(get_icon("warning.png"))) check_item.setToolTip(msg) else: - check_item.setIcon(QIcon(get_icon('invalid.png'))) + check_item.setIcon(QIcon(get_icon("invalid.png"))) check_item.setToolTip(msg) self.setItem(row, check_col, check_item) self.resizeColumnToContents(check_col) check_col = self.desc_cols.index(self.CHECK_LABEL) valid = True - msg = '' + msg = "" # --------------------------------------------------------------------- # ----------------- check if a variable is provided ------------------- @@ -996,7 +1076,7 @@ class ArrayTable(DragDropTable): var_item = self.item(row, self.desc_cols.index(self.VARIABLE_LABEL)) if var_item is not None and not asstring(var_item.text()).strip(): valid = False - msg = 'At least one variable name must be provided!' + msg = "At least one variable name must be provided!" # --------------------------------------------------------------------- # ----------------- check for duplicates of array names --------------- @@ -1008,10 +1088,18 @@ class ArrayTable(DragDropTable): arr_name = arr_item.text() if arr_name not in ignore_duplicates: if not arr_name: - msg = 'An array name must be provided' + msg = "An array name must be provided" valid = False - elif (len([name for name in self.current_names - if name == arr_name]) > 1): + elif ( + len( + [ + name + for name in self.current_names + if name == arr_name + ] + ) + > 1 + ): valid = False msg = "Found duplicated entry of '%s'" % arr_name @@ -1022,9 +1110,10 @@ class ArrayTable(DragDropTable): if valid and self.plot_method is not None: rows = self.get_all_rows(row) checks, messages = self.plot_method.check_data( - self.get_ds(), - name=list(map(self._get_variables, rows)), - dims=list(map(self._get_dims, rows))) + self.get_ds(), + name=list(map(self._get_variables, rows)), + dims=list(map(self._get_dims, rows)), + ) for row2, valid, msg in zip(rows, checks, messages): set_check(row2, valid, msg) valid = checks[rows.index(row)] @@ -1037,14 +1126,15 @@ class ArrayTable(DragDropTable): def check_arrays(self, **kwargs): """Convenience function to check all arrays using the :meth:`check_array` method""" - return list(map(partial(self.check_array, **kwargs), - range(self.rowCount()))) + return list( + map(partial(self.check_array, **kwargs), range(self.rowCount())) + ) def _str2slice(self, s): s = s.strip() if not s: return [] - s = s.split(':') + s = s.split(":") if len(s) > 1: return range(*map(int, s[:3])) return [int(s[0])] @@ -1052,14 +1142,19 @@ class ArrayTable(DragDropTable): def _get_dims(self, row): start = len(self.desc_cols) ret = {} - for dim, item in zip(self.dims, - map(lambda col: self.item(row, col), - range(start, self.columnCount()))): + for dim, item in zip( + self.dims, + map( + lambda col: self.item(row, col), + range(start, self.columnCount()), + ), + ): if item: text = asstring(item.text()) if text: slices = list( - chain(*map(self._str2slice, text.split(',')))) + chain(*map(self._str2slice, text.split(","))) + ) if len(slices) == 1: slices = slices[0] ret[dim] = slices @@ -1067,8 +1162,10 @@ class ArrayTable(DragDropTable): def _get_variables(self, row): var_col = self.desc_cols.index(self.VARIABLE_LABEL) - ret = [s.strip() for s in asstring( - self.item(row, var_col).text()).split(self.sep)] + ret = [ + s.strip() + for s in asstring(self.item(row, var_col).text()).split(self.sep) + ] ds = self.get_ds() for i, name in enumerate(ret): ret[i] = next(v for v in ds if asstring(v) == name) @@ -1081,8 +1178,9 @@ class SubplotCreator(QWidget): """Select a subplot to which will be created (if not already existing) when making the plot""" - def __init__(self, fig=None, rows=1, cols=1, num1=1, num2=None, *args, - **kwargs): + def __init__( + self, fig=None, rows=1, cols=1, num1=1, num2=None, *args, **kwargs + ): """ Parameters ---------- @@ -1099,31 +1197,37 @@ class SubplotCreator(QWidget): `num1` is used""" super(SubplotCreator, self).__init__(*args, **kwargs) - self.fig_label = QLabel('Figure number:', self) + self.fig_label = QLabel("Figure number:", self) if fig is None: import matplotlib.pyplot as plt + fig = next( - num for num in range(1, 1000) if num not in plt.get_fignums()) + num for num in range(1, 1000) if num not in plt.get_fignums() + ) self.fig_edit = QLineEdit(str(fig), self) self.fig_edit.setValidator(QIntValidator()) - self.rows_label = QLabel('No. of rows:', self) + self.rows_label = QLabel("No. of rows:", self) self.rows_edit = QLineEdit(str(rows), self) self.rows_edit.setValidator(QIntValidator(1, 9999, parent=self)) - self.cols_label = QLabel('No. of columns:', self) + self.cols_label = QLabel("No. of columns:", self) self.cols_edit = QLineEdit(str(cols), self) self.cols_edit.setValidator(QIntValidator(1, 9999, parent=self)) - self.num1_label = QLabel('Subplot number:', self) + self.num1_label = QLabel("Subplot number:", self) self.num1_edit = QLineEdit(str(num1), self) - self.num1_edit.setValidator(QIntValidator( - 1, max(1, (rows or 1)*(cols or 1)), self.num1_edit)) + self.num1_edit.setValidator( + QIntValidator(1, max(1, (rows or 1) * (cols or 1)), self.num1_edit) + ) - self.num2_label = QLabel('End of the plot', self) + self.num2_label = QLabel("End of the plot", self) self.num2_edit = QLineEdit(str(num2 or num1)) - self.num2_edit.setValidator(QIntValidator( - num1, max(1, (rows or 1)*(cols or 1)), self.num2_edit)) + self.num2_edit.setValidator( + QIntValidator( + num1, max(1, (rows or 1) * (cols or 1)), self.num2_edit + ) + ) self.table = table = QTableWidget(self) table.setSelectionMode(QAbstractItemView.ContiguousSelection) @@ -1182,8 +1286,15 @@ class SubplotCreator(QWidget): The new created subplot""" if not isinstance(fig, mpl.figure.Figure): import matplotlib.pyplot as plt - fig = plt.figure(fig or next( - num for num in range(1, 1000) if num not in plt.get_fignums())) + + fig = plt.figure( + fig + or next( + num + for num in range(1, 1000) + if num not in plt.get_fignums() + ) + ) if num1 == num2: num2 = None elif num2 is not None: @@ -1194,8 +1305,10 @@ class SubplotCreator(QWidget): for ax in fig.axes: ss = ax.get_subplotspec() if ss.num1 == num1 and ( - ss.num2 == num2 or (ss.num2 is None and num1 == num2) or - (num2 is None and ss.num2 == num1)): + ss.num2 == num2 + or (ss.num2 is None and num1 == num2) + or (num2 is None and ss.num2 == num1) + ): gs = ss.get_gridspec() if gs.get_geometry() == (rows, cols): return ax @@ -1216,8 +1329,10 @@ class SubplotCreator(QWidget): self.table.setColumnCount(cols) selected = int(self.num1_edit.text() or 0) if selected: - selected = (int(floor(selected / (cols + 1))), - ((selected % cols) - 1) % cols) + selected = ( + int(floor(selected / (cols + 1))), + ((selected % cols) - 1) % cols, + ) else: selected = (0, 0) for i, (row, col) in enumerate(product(range(rows), range(cols)), 1): @@ -1225,7 +1340,7 @@ class SubplotCreator(QWidget): self.table.setItem(row, col, item) self.table.resizeColumnsToContents() self.table.resizeRowsToContents() - self.num1_edit.validator().setTop(max(1, rows*cols)) + self.num1_edit.validator().setTop(max(1, rows * cols)) self.set_num2_validator(self.num1_edit.text()) self.set_selected_from_num1(self.num1_edit.text()) @@ -1237,7 +1352,8 @@ class SubplotCreator(QWidget): num2 = int(self.num2_edit.text() or num1) self.num2_edit.setText(str(max(num1, num2))) self.num2_edit.validator().setRange( - num1, max(1, (rows or 1)*(cols or 1))) + num1, max(1, (rows or 1) * (cols or 1)) + ) def set_selected_from_num1(self, s): """Update the selection of the table after changes of @@ -1250,8 +1366,7 @@ class SubplotCreator(QWidget): self.set_selected(num1, num2) def set_selected_from_num2(self, s): - """Update the selection of the table after changes of :attr:`num2_edit` - """ + """Update the selection of the table after changes of :attr:`num2_edit`""" self.table.clearSelection() if not s: return @@ -1268,10 +1383,12 @@ class SubplotCreator(QWidget): cols = int(self.cols_edit.text() or 0) if not rows or not cols: return - sel_rows = range(int(floor(num1 / (cols + 1))), - int(floor(num2 / (cols + 1))) + 1) - sel_cols = range(((num1 % cols) - 1) % cols, - (((num2 % cols) - 1) % cols) + 1) + sel_rows = range( + int(floor(num1 / (cols + 1))), int(floor(num2 / (cols + 1))) + 1 + ) + sel_cols = range( + ((num1 % cols) - 1) % cols, (((num2 % cols) - 1) % cols) + 1 + ) for item in starmap(self.table.item, product(sel_rows, sel_cols)): if item: self.table.blockSignals(True) @@ -1301,14 +1418,28 @@ class SubplotCreator(QWidget): figs = repeat(fig_text) else: import matplotlib.pyplot as plt - figs = map(str, (num for num in range(1, 1000) - if num not in plt.get_fignums())) - num1 = self.num1_edit.text() or '1' + + figs = map( + str, + ( + num + for num in range(1, 1000) + if num not in plt.get_fignums() + ), + ) + num1 = self.num1_edit.text() or "1" num2 = self.num2_edit.text() or num1 - return ('(%s, %s, %s, %s, %s)' % ( - fig, self.rows_edit.text() or '1', - self.cols_edit.text() or '1', num1, num2) - for fig in figs) + return ( + "(%s, %s, %s, %s, %s)" + % ( + fig, + self.rows_edit.text() or "1", + self.cols_edit.text() or "1", + num1, + num2, + ) + for fig in figs + ) class AxesViewer(QGraphicsView): @@ -1324,15 +1455,17 @@ class AxesViewer(QGraphicsView): def resizeEvent(self, *args, **kwargs): super(AxesViewer, self).resizeEvent(*args, **kwargs) self.setSceneRect( - 0, 0, self.frameSize().width(), self.frameSize().height()) + 0, 0, self.frameSize().width(), self.frameSize().height() + ) self.sizeChanged.emit(self.size()) class AxesCreator(QWidget): """Widget to setup an axes in a arbitrary location""" - def __init__(self, fig=None, x0=0.125, y0=0.1, x1=0.9, y1=0.9, - *args, **kwargs): + def __init__( + self, fig=None, x0=0.125, y0=0.1, x1=0.9, y1=0.9, *args, **kwargs + ): """ Parameters ---------- @@ -1348,33 +1481,31 @@ class AxesCreator(QWidget): the y-coordinate of the upper right corner (between 0 and 1) """ super(AxesCreator, self).__init__(*args, **kwargs) - self.fig_label = QLabel('Figure number:', self) + self.fig_label = QLabel("Figure number:", self) if fig is None: import matplotlib.pyplot as plt + fig = next( - num for num in range(1, 1000) if num not in plt.get_fignums()) + num for num in range(1, 1000) if num not in plt.get_fignums() + ) self.fig_edit = QLineEdit(str(fig), self) self.fig_edit.setValidator(QIntValidator()) - self.x0_label = QLabel('Lower left x: ', self) + self.x0_label = QLabel("Lower left x: ", self) self.x0_edit = QLineEdit(str(x0), self) - self.x0_edit.setValidator(QDoubleValidator(0.0, 1.0, 5, - parent=self)) + self.x0_edit.setValidator(QDoubleValidator(0.0, 1.0, 5, parent=self)) - self.y0_label = QLabel('Lower left y: ', self) + self.y0_label = QLabel("Lower left y: ", self) self.y0_edit = QLineEdit(str(y0), self) - self.y0_edit.setValidator(QDoubleValidator(0.0, 1.0, 5, - parent=self)) + self.y0_edit.setValidator(QDoubleValidator(0.0, 1.0, 5, parent=self)) - self.x1_label = QLabel('Upper right x: ', self) + self.x1_label = QLabel("Upper right x: ", self) self.x1_edit = QLineEdit(str(x1), self) - self.x1_edit.setValidator(QDoubleValidator(0.0, 1.0, 5, - parent=self)) + self.x1_edit.setValidator(QDoubleValidator(0.0, 1.0, 5, parent=self)) - self.y1_label = QLabel('Upper right y: ', self) + self.y1_label = QLabel("Upper right y: ", self) self.y1_edit = QLineEdit(str(y1), self) - self.y1_edit.setValidator(QDoubleValidator(0.5, 1.0, 5, - parent=self)) + self.y1_edit.setValidator(QDoubleValidator(0.5, 1.0, 5, parent=self)) self.graphics_scene = QGraphicsScene(self) self.graphics_view = AxesViewer(self.graphics_scene) @@ -1386,7 +1517,8 @@ class AxesCreator(QWidget): y0_resized = size.height() * float(y0) self.box_widget = QGraphicsRectItem( - x0_resized, y0_resized, width, height) + x0_resized, y0_resized, width, height + ) self.graphics_scene.addItem(self.box_widget) self.graphics_view.sizeChanged.connect(self.resize_rectangle) @@ -1405,15 +1537,20 @@ class AxesCreator(QWidget): layout.addWidget(self.graphics_view, 1, 2, 4, 4) for w in [self.x0_edit, self.y0_edit, self.x1_edit, self.y1_edit]: - w.textChanged.connect(lambda s: self.resize_rectangle( - self.graphics_view.size())) + w.textChanged.connect( + lambda s: self.resize_rectangle(self.graphics_view.size()) + ) self.setLayout(layout) def resize_rectangle(self, size): """resize the rectangle after changes of the widget size""" - coords = [self.x0_edit.text(), self.y0_edit.text(), - self.x1_edit.text(), self.y1_edit.text()] + coords = [ + self.x0_edit.text(), + self.y0_edit.text(), + self.x1_edit.text(), + self.y1_edit.text(), + ] if any(not c for c in coords): return x0, y0, x1, y1 = map(float, coords) @@ -1446,8 +1583,15 @@ class AxesCreator(QWidget): """ if not isinstance(fig, mpl.figure.Figure): import matplotlib.pyplot as plt - fig = plt.figure(fig or next( - num for num in range(1, 1000) if num not in plt.get_fignums())) + + fig = plt.figure( + fig + or next( + num + for num in range(1, 1000) + if num not in plt.get_fignums() + ) + ) x1 = max([x0, x1]) y1 = max([y0, y1]) bbox = mpl.transforms.Bbox.from_extents(x0, y0, x1, y1) @@ -1464,14 +1608,23 @@ class AxesCreator(QWidget): figs = repeat(fig_text) else: import matplotlib.pyplot as plt - figs = map(str, (num for num in range(1, 1000) - if num not in plt.get_fignums())) - left = self.x0_edit.text() or '0.125' - bottom = self.y0_edit.text() or '0.1' - width = self.x1_edit.text() or '0.9' - height = self.y1_edit.text() or '0.9' - return ('(%s, %s, %s, %s, %s)' % (fig, left, bottom, width, height) - for fig in figs) + + figs = map( + str, + ( + num + for num in range(1, 1000) + if num not in plt.get_fignums() + ), + ) + left = self.x0_edit.text() or "0.125" + bottom = self.y0_edit.text() or "0.1" + width = self.x1_edit.text() or "0.9" + height = self.y1_edit.text() or "0.9" + return ( + "(%s, %s, %s, %s, %s)" % (fig, left, bottom, width, height) + for fig in figs + ) class AxesSelector(QWidget): @@ -1481,11 +1634,11 @@ class AxesSelector(QWidget): def __init__(self, *args, **kwargs): super(AxesSelector, self).__init__(*args, **kwargs) - self.bt_choose = QPushButton('Click to select axes', self) + self.bt_choose = QPushButton("Click to select axes", self) self.bt_choose.setCheckable(True) - self.msg_label = QLabel('', self) + self.msg_label = QLabel("", self) - self.result_label = QLabel('', self) + self.result_label = QLabel("", self) self.layout = QVBoxLayout() self.layout.addWidget(self.bt_choose) @@ -1502,13 +1655,13 @@ class AxesSelector(QWidget): If the push button is clicked, we replace the existing pickers of the axes in order to select the plots. Otherwise we restore them""" if self.bt_choose.isChecked(): - self.bt_choose.setText('Click when finished') - self.msg_label.setText('Select an existing axes') - self.result_label.setText('') + self.bt_choose.setText("Click when finished") + self.msg_label.setText("Select an existing axes") + self.result_label.setText("") self.allow_axes_select() else: - self.bt_choose.setText('Select an axes') - self.msg_label.setText('') + self.bt_choose.setText("Select an axes") + self.msg_label.setText("") self.restore_pickers() def unclick(self): @@ -1519,11 +1672,12 @@ class AxesSelector(QWidget): def allow_axes_select(self): """Replace make all axes pickable""" import matplotlib.pyplot as plt + self.fig_events = d = {} self.pickers = pickers = defaultdict(dict) for num in plt.get_fignums(): fig = plt.figure(num) - d[num] = fig.canvas.mpl_connect('pick_event', self.get_picked_ax) + d[num] = fig.canvas.mpl_connect("pick_event", self.get_picked_ax) for ax in fig.axes: pickers[num][ax] = ax.get_picker() ax.set_picker(True) @@ -1531,6 +1685,7 @@ class AxesSelector(QWidget): def restore_pickers(self): """Restore the original pickers of the existing axes instances""" import matplotlib.pyplot as plt + for num, cid in self.fig_events.items(): plt.figure(num).canvas.mpl_disconnect(cid) for artist, picker in self.pickers[num].items(): @@ -1546,43 +1701,46 @@ class AxesSelector(QWidget): ax = event.artist.get_axes() text = self.result_label.text() if text: - text += ';;' - self.result_label.setText( - text + self.inspect_axes(ax)) + text += ";;" + self.result_label.setText(text + self.inspect_axes(ax)) def inspect_axes(self, ax): """Inspect the given axes and get the right string for making a plot with it""" from matplotlib.axes import SubplotBase + if isinstance(ax, SubplotBase): ss = ax.get_subplotspec() gs = ss.get_gridspec() rows, cols = gs.get_geometry() - return '(%i, %i, %i, %i, %i)' % ( - ax.get_figure().number, rows, cols, ss.num1 + 1, - (ss.num2 or ss.num1) + 1) + return "(%i, %i, %i, %i, %i)" % ( + ax.get_figure().number, + rows, + cols, + ss.num1 + 1, + (ss.num2 or ss.num1) + 1, + ) else: box = ax.get_position() points = np.round(box.get_points().ravel(), 5).tolist() - return '(%i, %1.5f, %1.5f, %1.5f, %1.5f)' % tuple( - [ax.get_figure().number] + points) + return "(%i, %1.5f, %1.5f, %1.5f, %1.5f)" % tuple( + [ax.get_figure().number] + points + ) def setVisible(self, b): - """Reimplemented to restore the pickers if the widget is made invisible - """ + """Reimplemented to restore the pickers if the widget is made invisible""" super(AxesSelector, self).setVisible(b) if not self.isVisible(): self.unclick() def close(self): - """Reimplemented to restore the pickers if the widget is closed - """ + """Reimplemented to restore the pickers if the widget is closed""" self.unclick() return super(AxesSelector, self).close() def get_iter(self): """Get the iterator over the axes""" - return (txt for txt in cycle(self.result_label.text().split(';;'))) + return (txt for txt in cycle(self.result_label.text().split(";;"))) class AxesCreatorCollection(QDialog): @@ -1599,9 +1757,11 @@ class AxesCreatorCollection(QDialog): #: key, title and class fot the widget that is used to create an #: axes - widgets = [('subplot', 'Subplot in a grid', SubplotCreator), - ('axes', 'Arbitray position', AxesCreator), - ('choose', 'Existing subplot', AxesSelector)] + widgets = [ + ("subplot", "Subplot in a grid", SubplotCreator), + ("axes", "Arbitray position", AxesCreator), + ("choose", "Existing subplot", AxesSelector), + ] def __init__(self, key=None, func_kwargs={}, *args, **kwargs): """ @@ -1616,8 +1776,8 @@ class AxesCreatorCollection(QDialog): ``*args,**kwargs`` Determined by the QWidget class""" super(AxesCreatorCollection, self).__init__(*args, **kwargs) - self.bt_cancel = QPushButton('Cancel', self) - self.bt_ok = QPushButton('Ok', self) + self.bt_cancel = QPushButton("Cancel", self) + self.bt_ok = QPushButton("Ok", self) self.tb = QTabWidget(self) self.tb.setTabPosition(QTabWidget.West) @@ -1668,15 +1828,15 @@ class PlotCreator(QDialog): Widget to extract data from a dataset and eventually create a plot""" #: Tooltip for not making a plot - NO_PM_TT = 'Choose a plot method (or choose none to only extract the data)' + NO_PM_TT = "Choose a plot method (or choose none to only extract the data)" _preset = None def __init__(self, *args, **kwargs): - self.help_explorer = kwargs.pop('help_explorer', None) + self.help_explorer = kwargs.pop("help_explorer", None) super(PlotCreator, self).__init__(*args, **kwargs) self.setAttribute(Qt.WA_DeleteOnClose) - self.setWindowTitle('Create plots') + self.setWindowTitle("Create plots") self.error_msg = PyErrorMessage(self) mp = psy.gcp(True) @@ -1689,23 +1849,24 @@ class PlotCreator(QDialog): # --------------------------------------------------------------------- self.ds_combo = QComboBox(parent=w) - self.ds_combo.setToolTip('The data source to use the data from') + self.ds_combo.setToolTip("The data source to use the data from") self.fill_ds_combo(mp) self.bt_open_file = QToolButton(parent=w) - self.bt_open_file.setIcon(QIcon(get_icon('run_arrow.png'))) - self.bt_open_file.setToolTip('Open a new dataset from the hard disk') + self.bt_open_file.setIcon(QIcon(get_icon("run_arrow.png"))) + self.bt_open_file.setToolTip("Open a new dataset from the hard disk") self.bt_get_ds = LoadFromConsoleButton(xarray.Dataset, parent=w) self.bt_get_ds.setToolTip( - 'Use a dataset already defined in the console') + "Use a dataset already defined in the console" + ) - self.pm_label = QLabel('Plot method: ', w) + self.pm_label = QLabel("Plot method: ", w) self.pm_combo = QComboBox(w) self.fill_plot_method_combo() self.bt_load_preset = QPushButton("Preset") self.bt_load_preset.setEnabled(False) self.pm_info = QToolButton(w) - self.pm_info.setIcon(QIcon(get_icon('info.png'))) - self.pm_info.setToolTip('Show information in the help explorer') + self.pm_info.setIcon(QIcon(get_icon("info.png"))) + self.pm_info.setToolTip("Show information in the help explorer") self.variables_table = VariablesTable(self.get_ds, parent=w) self.variables_table.fill_from_ds() @@ -1716,68 +1877,76 @@ class PlotCreator(QDialog): self.array_table = ArrayTable(self.get_ds, parent=w) self.array_table.setup_from_ds(plot_method=self.pm_combo.currentText()) - self.cbox_load = QCheckBox('load') + self.cbox_load = QCheckBox("load") self.cbox_load.setToolTip( - 'Load the selected data arrays into memory when clicking on ' - '<em>Ok</em>. Note that this might cause problems for large ' - 'arrays!') + "Load the selected data arrays into memory when clicking on " + "<em>Ok</em>. Note that this might cause problems for large " + "arrays!" + ) - self.cbox_close_popups = QCheckBox('close dropdowns', w) + self.cbox_close_popups = QCheckBox("close dropdowns", w) self.cbox_close_popups.setChecked(True) self.cbox_close_popups.setToolTip( - 'Close drop down menues after selecting indices to plot') - self.cbox_use_coords = QCheckBox('show coordinates', w) + "Close drop down menues after selecting indices to plot" + ) + self.cbox_use_coords = QCheckBox("show coordinates", w) self.cbox_use_coords.setChecked(False) self.cbox_use_coords.setToolTip( - 'Show the real coordinates instead of the indices in the drop ' - 'down menues') + "Show the real coordinates instead of the indices in the drop " + "down menues" + ) self.bt_remove_all = QToolButton(w) - self.bt_remove_all.setIcon(QIcon(get_icon('minusminus.png'))) - self.bt_remove_all.setToolTip('Remove all arrays') + self.bt_remove_all.setIcon(QIcon(get_icon("minusminus.png"))) + self.bt_remove_all.setToolTip("Remove all arrays") self.bt_remove = QToolButton(w) - self.bt_remove.setIcon(QIcon(get_icon('minus.png'))) - self.bt_remove.setToolTip('Remove selected arrays') + self.bt_remove.setIcon(QIcon(get_icon("minus.png"))) + self.bt_remove.setToolTip("Remove selected arrays") self.bt_add = QToolButton(w) - self.bt_add.setIcon(QIcon(get_icon('plus.png'))) - self.bt_add.setToolTip('Add arrays for the selected variables') + self.bt_add.setIcon(QIcon(get_icon("plus.png"))) + self.bt_add.setToolTip("Add arrays for the selected variables") self.bt_add_all = QToolButton(w) - self.bt_add_all.setIcon(QIcon(get_icon('plusplus.png'))) + self.bt_add_all.setIcon(QIcon(get_icon("plusplus.png"))) self.bt_add_all.setToolTip( - 'Add arrays for all variables in the dataset') + "Add arrays for all variables in the dataset" + ) - self.rows_axis_label = QLabel('No. of rows', w) + self.rows_axis_label = QLabel("No. of rows", w) self.rows_axis_edit = QLineEdit(w) - self.rows_axis_edit.setText('1') - self.cols_axis_label = QLabel('No. sof columns', w) + self.rows_axis_edit.setText("1") + self.cols_axis_label = QLabel("No. sof columns", w) self.cols_axis_edit = QLineEdit(w) - self.cols_axis_edit.setText('1') - self.max_axis_label = QLabel('No. of axes per figure', w) + self.cols_axis_edit.setText("1") + self.max_axis_label = QLabel("No. of axes per figure", w) self.max_axis_edit = QLineEdit(w) - self.bt_add_axes = QPushButton('Add new subplots', w) + self.bt_add_axes = QPushButton("Add new subplots", w) self.bt_add_axes.setToolTip( - 'Adds subplots for the selected arrays based the specified number ' - 'of rows and columns') + "Adds subplots for the selected arrays based the specified number " + "of rows and columns" + ) - self.row_axis_label = QLabel('Row number:', w) + self.row_axis_label = QLabel("Row number:", w) self.row_axis_edit = QLineEdit(w) - self.row_axis_edit.setText('1') - self.col_axis_label = QLabel('Column number', w) + self.row_axis_edit.setText("1") + self.col_axis_label = QLabel("Column number", w) self.col_axis_edit = QLineEdit(w) - self.col_axis_edit.setText('1') - self.bt_add_single_axes = QPushButton('Add one subplot', w) + self.col_axis_edit.setText("1") + self.bt_add_single_axes = QPushButton("Add one subplot", w) self.bt_add_single_axes.setToolTip( - 'Add one subplot for the specified row and column') + "Add one subplot for the specified row and column" + ) self.fmt_tree_label = QLabel( "Modify the formatoptions of the newly created plots." "Values must be entered in yaml syntax", - parent=self.fmt_tree_widget) + parent=self.fmt_tree_widget, + ) - self.fmt_tree = RcParamsTree(None, None, None, - parent=self.fmt_tree_widget) + self.fmt_tree = RcParamsTree( + None, None, None, parent=self.fmt_tree_widget + ) self.fmt_tree.value_col = 3 self.fmt_tree.setColumnCount(4) - self.fmt_tree.setHeaderLabels(['Formatoption', '', '', 'Value']) + self.fmt_tree.setHeaderLabels(["Formatoption", "", "", "Value"]) # --------------------------------------------------------------------- # ---------------------------- connections ---------------------------- @@ -1789,18 +1958,24 @@ class PlotCreator(QDialog): self.ds_combo.currentIndexChanged[int].connect(self.set_ds) self.ds_combo.currentIndexChanged[int].connect( - lambda i: self.variables_table.fill_from_ds()) + lambda i: self.variables_table.fill_from_ds() + ) self.ds_combo.currentIndexChanged[int].connect( - lambda i: self.coords_table.fill_from_ds()) + lambda i: self.coords_table.fill_from_ds() + ) self.ds_combo.currentIndexChanged[int].connect( - lambda i: self.array_table.setup_from_ds()) + lambda i: self.array_table.setup_from_ds() + ) self.ds_combo.currentIndexChanged[int].connect( - lambda i: self.connect_combo_boxes()) + lambda i: self.connect_combo_boxes() + ) # ------------------- plot method connections ------------------------- self.pm_combo.currentIndexChanged[str].connect( - lambda s: self.pm_combo.setToolTip( - getattr(psy.plot, s)._summary) if s else self.NO_PM_TT) + lambda s: self.pm_combo.setToolTip(getattr(psy.plot, s)._summary) + if s + else self.NO_PM_TT + ) self.pm_info.clicked.connect(self.show_pm_info) self.pm_combo.currentIndexChanged[str].connect(self.array_table.set_pm) self.pm_combo.currentIndexChanged[str].connect(self.fill_fmt_tree) @@ -1816,15 +1991,22 @@ class PlotCreator(QDialog): self.connect_combo_boxes() # ----------------- add and remove button connections ----------------- - self.bt_add.clicked.connect(lambda b: self.insert_array( - variables=self.variables_table.selected_variables)) + self.bt_add.clicked.connect( + lambda b: self.insert_array( + variables=self.variables_table.selected_variables + ) + ) self.bt_add_all.clicked.connect( lambda b: self.insert_array( - variables=self.variables_table.variables)) + variables=self.variables_table.variables + ) + ) self.bt_remove_all.clicked.connect( - lambda b: self.array_table.remove_arrays(False)) + lambda b: self.array_table.remove_arrays(False) + ) self.bt_remove.clicked.connect( - lambda b: self.array_table.remove_arrays(True)) + lambda b: self.array_table.remove_arrays(True) + ) # ------------- axes creation connections ----------------------------- self.rows_axis_edit.returnPressed.connect(self.bt_add_axes.click) @@ -1836,8 +2018,9 @@ class PlotCreator(QDialog): self.bt_add_single_axes.clicked.connect(self.setup_subplot) # -------------------- create and cancel connections ------------------ - self.bbox = bbox = QDialogButtonBox(QDialogButtonBox.Ok | - QDialogButtonBox.Cancel) + self.bbox = bbox = QDialogButtonBox( + QDialogButtonBox.Ok | QDialogButtonBox.Cancel + ) bbox.accepted.connect(self.create_plots) bbox.rejected.connect(self.reject) @@ -1846,9 +2029,11 @@ class PlotCreator(QDialog): # order to control the behaviour of the combo box left click in # self.insert_array_from_combo self.array_table.itemSelectionChanged.connect( - self.variables_table.clearSelection) + self.variables_table.clearSelection + ) self.variables_table.itemSelectionChanged.connect( - self.array_table.clearSelection) + self.array_table.clearSelection + ) # --------------------------------------------------------------------- # ---------------------------- layouts -------------------------------- @@ -1932,6 +2117,7 @@ class PlotCreator(QDialog): def fill_fmt_tree(self, pm): import psyplot.project as psy + self.fmt_tree.clear() if not pm: self.fmt_tree_widget.setVisible(False) @@ -1940,17 +2126,21 @@ class PlotCreator(QDialog): pm = getattr(psy.plot, pm) plotter = pm.plotter_cls() if self._preset: - plotter.update(psy.Project.extract_fmts_from_preset( - self._preset, pm)) + plotter.update( + psy.Project.extract_fmts_from_preset(self._preset, pm) + ) self.fmt_tree.rc = plotter self.fmt_tree.validators = { - key: getattr(plotter, key).validate for key in plotter} + key: getattr(plotter, key).validate for key in plotter + } self.fmt_tree.descriptions = { - key: getattr(plotter, key).name for key in plotter} + key: getattr(plotter, key).name for key in plotter + } self.fmt_tree.initialize() - icon = QIcon(get_icon('info.png')) + icon = QIcon(get_icon("info.png")) docs_funcs = { - key: partial(plotter.show_docs, key) for key in plotter} + key: partial(plotter.show_docs, key) for key in plotter + } for item in self.fmt_tree.top_level_items: key = item.text(0) bt = QToolButton() @@ -1984,13 +2174,17 @@ class PlotCreator(QDialog): return pm_name = self.pm_combo.currentText() if pm_name: - self.help_explorer.show_help(getattr(psy.plot, pm_name), - 'psyplot.project.plot.' + pm_name) + self.help_explorer.show_help( + getattr(psy.plot, pm_name), "psyplot.project.plot." + pm_name + ) else: - self.help_explorer.show_rst(""" + self.help_explorer.show_rst( + """ No plot ======= - No plot will be created, only the data is extracted""", 'no_plot') + No plot will be created, only the data is extracted""", + "no_plot", + ) def connect_combo_boxes(self): for cb in self.coords_table.combo_boxes: @@ -1998,12 +2192,13 @@ class PlotCreator(QDialog): def fill_plot_method_combo(self): """Takes the names of the plotting methods in the current project""" - self.pm_combo.addItems([''] + sorted(psy.plot._plot_methods)) + self.pm_combo.addItems([""] + sorted(psy.plot._plot_methods)) self.pm_combo.setToolTip(self.NO_PM_TT) def set_pm(self, plot_method): self.pm_combo.setCurrentIndex( - self.pm_combo.findText(plot_method or '')) + self.pm_combo.findText(plot_method or "") + ) def create_plots(self): """Method to be called when the `Create plot` button is pressed @@ -2012,37 +2207,48 @@ class PlotCreator(QDialog): makes the plot (or extracts the data) based upon the :attr:`plot_method` attribute""" import matplotlib.pyplot as plt + names = self.array_table.arr_names_dict pm = self.pm_combo.currentText() if pm: pm = getattr(psy.plot, pm) for d, (default_dim, default_slice) in product( - six.itervalues(names), six.iteritems(pm._default_dims)): + six.itervalues(names), six.iteritems(pm._default_dims) + ): d.setdefault(default_dim, default_slice) - kwargs = {'ax': self.array_table.axes, - 'fmt': {t[1]: t[2] for t in self.fmt_tree._get_rc()}} + kwargs = { + "ax": self.array_table.axes, + "fmt": {t[1]: t[2] for t in self.fmt_tree._get_rc()}, + } else: pm = self.open_data kwargs = {} fig_nums = plt.get_fignums()[:] try: - pm(self.ds, arr_names=names, load=self.cbox_load.isChecked(), - **kwargs) + pm( + self.ds, + arr_names=names, + load=self.cbox_load.isChecked(), + **kwargs, + ) except Exception: for num in set(plt.get_fignums()).difference(fig_nums): plt.close(num) - self.error_msg.showTraceback('<b>Failed to create the plots!</b>') - logger.debug('Error while creating the plots with %s!', - names, exc_info=True) + self.error_msg.showTraceback("<b>Failed to create the plots!</b>") + logger.debug( + "Error while creating the plots with %s!", names, exc_info=True + ) else: self.close() def load_preset(self): """Load a preset file""" fname, ok = QFileDialog.getOpenFileName( - self, 'Load preset', os.path.join(get_configdir(), 'presets'), - 'YAML files (*.yml *.yaml);;' - 'All files (*)') + self, + "Load preset", + os.path.join(get_configdir(), "presets"), + "YAML files (*.yml *.yaml);;" "All files (*)", + ) if ok: self.set_preset(fname) @@ -2051,91 +2257,98 @@ class PlotCreator(QDialog): def open_ds(): if len(fnames) == 1: - kwargs.pop('concat_dim', None) + kwargs.pop("concat_dim", None) return psy.open_dataset(fnames[0], *args, **kwargs) else: return psy.open_mfdataset(fnames, *args, **kwargs) if fnames is None: fnames = QFileDialog.getOpenFileNames( - self, 'Open dataset', os.getcwd(), - 'NetCDF files (*.nc *.nc4);;' - 'Shape files (*.shp);;' - 'All files (*)' - ) + self, + "Open dataset", + os.getcwd(), + "NetCDF files (*.nc *.nc4);;" + "Shape files (*.shp);;" + "All files (*)", + ) if with_qt5: # the filter is passed as well fnames = fnames[0] if isinstance(fnames, xarray.Dataset): ds = fnames - self.add_new_ds('ds', ds) + self.add_new_ds("ds", ds) elif not fnames: return else: try: ds = open_ds() except Exception: - kwargs['decode_times'] = False + kwargs["decode_times"] = False try: ds = open_ds() except Exception: self.error_msg.showTraceback( - '<b>Could not open dataset %s</b>' % (fnames, )) + "<b>Could not open dataset %s</b>" % (fnames,) + ) return - fnames_str = ', '.join(fnames) + fnames_str = ", ".join(fnames) self.add_new_ds(fnames_str, ds, fnames_str) def set_preset(self, preset): import psyplot.project as psy + self._preset = psy.Project._load_preset(preset) if self.fmt_tree_widget.isVisible(): self.fill_fmt_tree(self.pm_combo.currentText()) def add_new_ds(self, oname, ds, fname=None): - d = {'ds': ds} + d = {"ds": ds} if fname: - d['fname'] = fname + d["fname"] = fname self.ds_descs.insert(0, d) - self.ds_combo.insertItem(0, 'New: ' + oname) + self.ds_combo.insertItem(0, "New: " + oname) self.ds_combo.setCurrentIndex(0) def set_ds(self, i): """Set the current dataset""" - self.ds = self.ds_descs[i]['ds'] + self.ds = self.ds_descs[i]["ds"] def fill_ds_combo(self, project): - """fill the dataset combobox with datasets of the current main project - """ + """fill the dataset combobox with datasets of the current main project""" self.ds_combo.clear() self.ds_combo.setInsertPolicy(QComboBox.InsertAtBottom) ds_descs = project._get_ds_descriptions( - project.array_info(ds_description='all')) + project.array_info(ds_description="all") + ) self.ds_combo.addItems( - ['%i: %s' % (i, ds_desc['fname']) for i, ds_desc in six.iteritems( - ds_descs)]) + [ + "%i: %s" % (i, ds_desc["fname"]) + for i, ds_desc in six.iteritems(ds_descs) + ] + ) self.ds_descs = list(ds_descs.values()) if len(self.ds_descs): self.set_ds(0) def insert_array_from_combo(self, cb, variables=None): - """Insert new arrays into the dataset when the combobox is left-clicked - """ + """Insert new arrays into the dataset when the combobox is left-clicked""" if variables is None: variables = self.variables_table.selected_variables dims = {} for other_cb in self.coords_table.combo_boxes: ind = other_cb.currentIndex() - dims[other_cb.dim] = str((ind - 1) if ind not in [-1, 0] else '') + dims[other_cb.dim] = str((ind - 1) if ind not in [-1, 0] else "") dim = cb.dim inserts = list( str(ind.row() - 1) for ind in cb.view().selectionModel().selectedIndexes() - if ind.row() > 0) + if ind.row() > 0 + ) dims.pop(dim) for name, val in product(variables, inserts): dims[dim] = val self.array_table.insert_array(name, check=False, **dims) if len(inserts) > 1: - inserts = '%s:%s' % (min(inserts), max(inserts)) + inserts = "%s:%s" % (min(inserts), max(inserts)) elif inserts: inserts = inserts[0] else: @@ -2152,7 +2365,7 @@ class PlotCreator(QDialog): dims = {} for other_cb in self.coords_table.combo_boxes: ind = other_cb.currentIndex() - dims[other_cb.dim] = str((ind - 1) if ind not in [-1, 0] else '') + dims[other_cb.dim] = str((ind - 1) if ind not in [-1, 0] else "") for name in variables: self.array_table.insert_array(name, check=False, **dims) self.array_table.check_arrays() @@ -2175,12 +2388,12 @@ class PlotCreator(QDialog): i = self.ds_combo.currentIndex() if not len(self.ds_descs): return - return self.ds_descs[i]['ds'] + return self.ds_descs[i]["ds"] def close(self, *args, **kwargs): """Reimplemented to make sure that the data sets are deleted""" super(PlotCreator, self).close(*args, **kwargs) - if hasattr(self, 'ds_descs'): + if hasattr(self, "ds_descs"): del self.ds_descs def open_data(self, *args, **kwargs): @@ -2199,7 +2412,7 @@ class PlotCreator(QDialog): The dataset to use. It is assumed that this dataset is already in the dataset combobox""" for i, desc in enumerate(self.ds_descs): - if desc['ds'] is ds: + if desc["ds"] is ds: self.ds_combo.setCurrentIndex(i) return diff --git a/psyplot_gui/preferences.py b/psyplot_gui/preferences.py index 4c0aa2e64ea7df634c078a0773acf4979250ec00..f90e64f2708faf5fd22cb73e01407c092a634f9f 100644 --- a/psyplot_gui/preferences.py +++ b/psyplot_gui/preferences.py @@ -3,41 +3,49 @@ This module defines the :class:`Preferences` widget that creates an interface to the rcParams of psyplot and psyplot_gui""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only -import yaml from warnings import warn -from psyplot_gui.compat.qtcompat import ( - QTreeWidget, QTreeWidgetItem, Qt, QMenu, QAction, QTextEdit, QIcon, - QWidget, QVBoxLayout, QHBoxLayout, QtCore, QDialog, QScrollArea, - QDialogButtonBox, QStackedWidget, QListWidget, QListView, QSplitter, - QListWidgetItem, QPushButton, QFileDialog, with_qt5, - QAbstractItemView, QToolButton, QLabel, QtGui, asstring) -from psyplot_gui.common import get_icon + +import yaml +from psyplot.config.rcsetup import RcParams, psyplot_fname +from psyplot.config.rcsetup import rcParams as psy_rcParams + from psyplot_gui import rcParams as rcParams -from psyplot.config.rcsetup import ( - psyplot_fname, RcParams, rcParams as psy_rcParams) +from psyplot_gui.common import get_icon +from psyplot_gui.compat.qtcompat import ( + QAbstractItemView, + QAction, + QDialog, + QDialogButtonBox, + QFileDialog, + QHBoxLayout, + QIcon, + QLabel, + QListView, + QListWidget, + QListWidgetItem, + QMenu, + QPushButton, + QScrollArea, + QSplitter, + QStackedWidget, + Qt, + QtCore, + QTextEdit, + QtGui, + QToolButton, + QTreeWidget, + QTreeWidgetItem, + QVBoxLayout, + QWidget, + asstring, + with_qt5, +) class ConfigPage(object): @@ -127,7 +135,7 @@ class RcParamsTree(QTreeWidget): self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.open_menu) self.setColumnCount(self.value_col + 1) - self.setHeaderLabels(['RcParams key', '', 'Value']) + self.setHeaderLabels(["RcParams key", "", "Value"]) @property def is_valid(self): @@ -150,7 +158,7 @@ class RcParamsTree(QTreeWidget): item = QTreeWidgetItem(0) item.setText(0, key) item.setToolTip(0, key) - item.setIcon(1, QIcon(get_icon('valid.png'))) + item.setIcon(1, QIcon(get_icon("valid.png"))) desc = descriptions.get(key) if desc: item.setText(vcol, desc) @@ -161,11 +169,13 @@ class RcParamsTree(QTreeWidget): editor = QTextEdit(self) # set maximal height of the editor to 3 rows editor.setMaximumHeight( - 4 * QtGui.QFontMetrics(editor.font()).height()) + 4 * QtGui.QFontMetrics(editor.font()).height() + ) editor.setPlainText(yaml.dump(val)) self.setItemWidget(child, vcol, editor) editor.textChanged.connect( - self.set_icon_func(i, item, validators[key])) + self.set_icon_func(i, item, validators[key]) + ) self.resizeColumnToContents(0) self.resizeColumnToContents(1) @@ -191,26 +201,28 @@ class RcParamsTree(QTreeWidget): ------- function The function that can be called to set the correct icon""" + def func(): editor = self.itemWidget(item.child(0), self.value_col) s = asstring(editor.toPlainText()) try: val = yaml.load(s, Loader=yaml.Loader) except Exception as e: - item.setIcon(1, QIcon(get_icon('warning.png'))) + item.setIcon(1, QIcon(get_icon("warning.png"))) item.setToolTip(1, "Could not parse yaml code: %s" % e) self.set_valid(i, False) return try: validator(val) except Exception as e: - item.setIcon(1, QIcon(get_icon('invalid.png'))) + item.setIcon(1, QIcon(get_icon("invalid.png"))) item.setToolTip(1, "Wrong value: %s" % e) self.set_valid(i, False) else: - item.setIcon(1, QIcon(get_icon('valid.png'))) + item.setIcon(1, QIcon(get_icon("valid.png"))) self.set_valid(i, True) self.propose_changes.emit(self.parent() or self) + return func def set_valid(self, i, b): @@ -240,10 +252,10 @@ class RcParamsTree(QTreeWidget): position: QPosition The position where to open the menu""" menu = QMenu() - expand_all_action = QAction('Expand all', self) + expand_all_action = QAction("Expand all", self) expand_all_action.triggered.connect(self.expandAll) menu.addAction(expand_all_action) - collapse_all_action = QAction('Collapse all', self) + collapse_all_action = QAction("Collapse all", self) collapse_all_action.triggered.connect(self.collapseAll) menu.addAction(collapse_all_action) menu.exec_(self.viewport().mapToGlobal(position)) @@ -262,8 +274,10 @@ class RcParamsTree(QTreeWidget): The item identifier object The proposed value""" + def equals(item, key, val, orig): return val != orig + for t in self._get_rc(equals): yield t[0 if use_items else 1], t[2] @@ -281,8 +295,10 @@ class RcParamsTree(QTreeWidget): The item identifier object The proposed value""" + def is_selected(item, key, val, orig): return item.isSelected() + for t in self._get_rc(is_selected): yield t[0 if use_items else 1], t[2] @@ -317,8 +333,10 @@ class RcParamsTree(QTreeWidget): object The current value """ + def no_check(item, key, val, orig): return True + rc = self.rc filter_func = filter_func or no_check for item in self.top_level_items: @@ -327,13 +345,12 @@ class RcParamsTree(QTreeWidget): val = yaml.load(asstring(editor.toPlainText()), Loader=yaml.Loader) try: val = rc.validate[key](val) - except: + except Exception: pass try: include = filter_func(item, key, val, rc[key]) - except: - warn('Could not check state for %s key' % key, - RuntimeWarning) + except Exception: + warn("Could not check state for %s key" % key, RuntimeWarning) else: if include: yield (item, key, val, rc[key]) @@ -345,8 +362,7 @@ class RcParamsTree(QTreeWidget): self.rc.update(new) def select_changes(self): - """Select all the items that changed comparing to the current rcParams - """ + """Select all the items that changed comparing to the current rcParams""" for item, val in self.changed_rc(True): item.setSelected(True) @@ -392,29 +408,34 @@ class RcParamsWidget(ConfigPage, QWidget): @property def icon(self): """The icon of this instance in the :class:`Preferences` dialog""" - return QIcon(get_icon('rcParams.png')) + return QIcon(get_icon("rcParams.png")) def __init__(self, *args, **kwargs): super(RcParamsWidget, self).__init__(*args, **kwargs) self.vbox = vbox = QVBoxLayout() self.description = QLabel( - '<p>Modify the rcParams for your need. Changes will not be applied' - ' until you click the Apply or Ok button.</p>' - '<p>Values must be entered in yaml syntax</p>', parent=self) + "<p>Modify the rcParams for your need. Changes will not be applied" + " until you click the Apply or Ok button.</p>" + "<p>Values must be entered in yaml syntax</p>", + parent=self, + ) vbox.addWidget(self.description) self.tree = tree = RcParamsTree( - self.rc, getattr(self.rc, 'validate', None), - getattr(self.rc, 'descriptions', None), parent=self) + self.rc, + getattr(self.rc, "validate", None), + getattr(self.rc, "descriptions", None), + parent=self, + ) tree.setSelectionMode(QAbstractItemView.MultiSelection) vbox.addWidget(self.tree) - self.bt_select_all = QPushButton('Select All', self) - self.bt_select_changed = QPushButton('Select changes', self) - self.bt_select_none = QPushButton('Clear Selection', self) + self.bt_select_all = QPushButton("Select All", self) + self.bt_select_changed = QPushButton("Select changes", self) + self.bt_select_none = QPushButton("Clear Selection", self) self.bt_export = QToolButton(self) - self.bt_export.setText('Export Selection...') - self.bt_export.setToolTip('Export the selected rcParams to a file') + self.bt_export.setText("Export Selection...") + self.bt_export.setToolTip("Export the selected rcParams to a file") self.bt_export.setPopupMode(QToolButton.InstantPopup) self.export_menu = export_menu = QMenu(self) export_menu.addAction(self.save_settings_action()) @@ -443,6 +464,7 @@ class RcParamsWidget(ConfigPage, QWidget): If True, it is expected that the file already exists and it will be updated. Otherwise, existing files will be overwritten """ + def func(): if update: meth = QFileDialog.getOpenFileName @@ -450,12 +472,11 @@ class RcParamsWidget(ConfigPage, QWidget): meth = QFileDialog.getSaveFileName if target is None: fname = meth( - self, 'Select a file to %s' % ( - 'update' if update else 'create'), + self, + "Select a file to %s" % ("update" if update else "create"), self.default_path, - 'YAML files (*.yml);;' - 'All files (*)' - ) + "YAML files (*.yml);;" "All files (*)", + ) if with_qt5: # the filter is passed as well fname = fname[0] else: @@ -469,14 +490,17 @@ class RcParamsWidget(ConfigPage, QWidget): selected = dict(self.tree.selected_rc()) new_keys = list(selected) rc.update(selected) - rc.dump(fname, include_keys=old_keys + new_keys, - exclude_keys=[]) + rc.dump( + fname, include_keys=old_keys + new_keys, exclude_keys=[] + ) else: - rc = self.rc.__class__(self.tree.selected_rc(), - defaultParams=self.rc.defaultParams) + rc = self.rc.__class__( + self.tree.selected_rc(), + defaultParams=self.rc.defaultParams, + ) rc.dump(fname, exclude_keys=[]) - action = QAction('Update...' if update else 'Overwrite...', self) + action = QAction("Update..." if update else "Overwrite...", self) action.triggered.connect(func) return action @@ -516,10 +540,11 @@ class GuiRcParamsWidget(RcParamsWidget): rc = rcParams - title = 'GUI defaults' + title = "GUI defaults" - default_path = psyplot_fname('PSYPLOTGUIRC', 'psyplotguirc.yml', - if_exists=False) + default_path = psyplot_fname( + "PSYPLOTGUIRC", "psyplotguirc.yml", if_exists=False + ) class PsyRcParamsWidget(RcParamsWidget): @@ -527,7 +552,7 @@ class PsyRcParamsWidget(RcParamsWidget): rc = psy_rcParams - title = 'psyplot defaults' + title = "psyplot defaults" default_path = psyplot_fname(if_exists=False) @@ -545,19 +570,22 @@ class Prefences(QDialog): def __init__(self, main=None): super(Prefences, self).__init__(parent=main) - self.setWindowTitle('Preferences') + self.setWindowTitle("Preferences") # Widgets self.pages_widget = QStackedWidget() self.contents_widget = QListWidget() - self.bt_reset = QPushButton('Reset to defaults') - self.bt_load_plugins = QPushButton('Load plugin pages') + self.bt_reset = QPushButton("Reset to defaults") + self.bt_load_plugins = QPushButton("Load plugin pages") self.bt_load_plugins.setToolTip( - 'Load the rcParams for the plugins in separate pages') + "Load the rcParams for the plugins in separate pages" + ) self.bbox = bbox = QDialogButtonBox( - QDialogButtonBox.Ok | QDialogButtonBox.Apply | - QDialogButtonBox.Cancel) + QDialogButtonBox.Ok + | QDialogButtonBox.Apply + | QDialogButtonBox.Cancel + ) # Widgets setup # Destroying the C++ object right after closing the dialog box, @@ -565,7 +593,7 @@ class Prefences(QDialog): # (e.g. the editor's analysis thread in Spyder), thus leading to # a segmentation fault on UNIX or an application crash on Windows self.setAttribute(Qt.WA_DeleteOnClose) - self.setWindowTitle('Preferences') + self.setWindowTitle("Preferences") self.contents_widget.setMovement(QListView.Static) self.contents_widget.setSpacing(1) self.contents_widget.setCurrentRow(0) @@ -594,7 +622,8 @@ class Prefences(QDialog): self.bt_load_plugins.clicked.connect(self.load_plugin_pages) self.pages_widget.currentChanged.connect(self.current_page_changed) self.contents_widget.currentRowChanged.connect( - self.pages_widget.setCurrentIndex) + self.pages_widget.setCurrentIndex + ) bbox.accepted.connect(self.accept) bbox.rejected.connect(self.reject) self.bt_apply.clicked.connect(self.apply_clicked) @@ -641,7 +670,8 @@ class Prefences(QDialog): The page to add""" widget.validChanged.connect(self.bt_apply.setEnabled) widget.validChanged.connect( - self.bbox.button(QDialogButtonBox.Ok).setEnabled) + self.bbox.button(QDialogButtonBox.Ok).setEnabled + ) scrollarea = QScrollArea(self) scrollarea.setWidgetResizable(True) scrollarea.setWidget(widget) @@ -661,8 +691,10 @@ class Prefences(QDialog): if configpage != self.get_page(): return self.bt_apply.setEnabled( - not configpage.auto_updates and configpage.is_valid and - configpage.changed) + not configpage.auto_updates + and configpage.is_valid + and configpage.changed + ) def load_plugin_pages(self): """Load the rcParams for the plugins in separate pages""" @@ -670,14 +702,15 @@ class Prefences(QDialog): descriptions = psy_rcParams.descriptions for ep in psy_rcParams._load_plugin_entrypoints(): plugin = ep.load() - rc = getattr(plugin, 'rcParams', None) + rc = getattr(plugin, "rcParams", None) if rc is None: rc = RcParams() w = RcParamsWidget(parent=self) - w.title = 'rcParams of ' + ep.module_name + w.title = "rcParams of " + ep.module w.default_path = PsyRcParamsWidget.default_path - w.initialize(rcParams=rc, validators=validators, - descriptions=descriptions) + w.initialize( + rcParams=rc, validators=validators, descriptions=descriptions + ) # use the full rcParams after initialization w.rc = psy_rcParams self.add_page(w) diff --git a/psyplot_gui/sphinx_supp/__init__.py b/psyplot_gui/sphinx_supp/__init__.py index abd809acf13df85ab140857f614942842bd2676e..bf40e2ede925d44922fc2a039a6e3620d5761e26 100644 --- a/psyplot_gui/sphinx_supp/__init__.py +++ b/psyplot_gui/sphinx_supp/__init__.py @@ -1,24 +1,7 @@ """Supplementary files for building the docs with sphinx.""" -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: LGPL-3.0-only diff --git a/psyplot_gui/sphinx_supp/conf.py b/psyplot_gui/sphinx_supp/conf.py index fbab1840c72aac2d574ad9e94499c230db6813e8..658a18a699ff38322da42fe7c3bff5738987bd07 100755 --- a/psyplot_gui/sphinx_supp/conf.py +++ b/psyplot_gui/sphinx_supp/conf.py @@ -12,36 +12,16 @@ # All configuration values have a default; values that are commented out # serve to show the default. -# Disclaimer -# ---------- +# SPDX-FileCopyrightText: 2016-2024 University of Lausanne +# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. - +# SPDX-License-Identifier: LGPL-3.0-only -import sys -import sphinx -import sphinx_rtd_theme import re -import six +import sys from itertools import product + import psyplot_gui # -- General configuration ------------------------------------------------ @@ -50,57 +30,57 @@ import psyplot_gui # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autosummary', - 'sphinx.ext.todo', - 'sphinx.ext.viewcode', - 'psyplot.sphinxext.extended_napoleon', + "sphinx.ext.autosummary", + "sphinx.ext.todo", + "sphinx.ext.viewcode", + "psyplot.sphinxext.extended_napoleon", ] -if psyplot_gui.rcParams['help_explorer.use_intersphinx'] is None: +if psyplot_gui.rcParams["help_explorer.use_intersphinx"] is None: if sys.platform.startswith("win"): use_intersphinx = False else: - use_intersphinx = psyplot_gui.rcParams['help_explorer.online'] + use_intersphinx = psyplot_gui.rcParams["help_explorer.online"] else: - use_intersphinx = psyplot_gui.rcParams['help_explorer.use_intersphinx'] + use_intersphinx = psyplot_gui.rcParams["help_explorer.use_intersphinx"] if use_intersphinx: - extensions.append('sphinx.ext.intersphinx') + extensions.append("sphinx.ext.intersphinx") del use_intersphinx -autodoc_default_options = { - 'show_inheritance': True -} +autodoc_default_options = {"show_inheritance": True} try: - import autodocsumm + import autodocsumm # noqa: F401 except ImportError: pass else: - extensions.append('autodocsumm') - autodoc_default_options['autosummary'] = True - not_document_data = ['psyplot.config.rcsetup.defaultParams', - 'psyplot.config.rcsetup.rcParams'] + extensions.append("autodocsumm") + autodoc_default_options["autosummary"] = True + not_document_data = [ + "psyplot.config.rcsetup.defaultParams", + "psyplot.config.rcsetup.rcParams", + ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] napoleon_use_admonition_for_examples = True # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'psyplot' +master_doc = "psyplot" -autoclass_content = 'both' +autoclass_content = "both" # General information about the project. -project = 'psyplot Help' +project = "psyplot Help" copyright = psyplot_gui.__copyright__ author = psyplot_gui.__author__ @@ -109,7 +89,7 @@ author = psyplot_gui.__author__ # built documents. # # The short X.Y version. -version = re.match(r'\d+\.\d+\.\d+', psyplot_gui.__version__).group() +version = re.match(r"\d+\.\d+\.\d+", psyplot_gui.__version__).group() # The full version, including alpha/beta/rc tags. release = psyplot_gui.__version__ # @@ -119,62 +99,62 @@ language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] +html_theme = "sphinx_rtd_theme" html_theme_options = { - 'prev_next_buttons_location': None - } + "prev_next_buttons_location": None, + "collapse_navigation": False, +} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Output file base name for HTML help builder. -htmlhelp_basename = 'psyplotdoc' +htmlhelp_basename = "psyplotdoc" # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'matplotlib': ('https://matplotlib.org/', None), - 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), - 'xarray': ('https://xarray.pydata.org/en/stable/', None), - 'cartopy': ('https://scitools.org.uk/cartopy/docs/latest/', None), - 'psyplot': ('https://psyplot.github.io/psyplot/', None), - 'psyplot_gui': ('https://psyplot.github.io/psyplot-gui/', None), - 'psy_maps': ('https://psyplot.github.io/psy-maps/', None), - 'psy_simple': ('https://psyplot.github.io/psy-simple/', None), - 'psy_view': ('https://psyplot.github.io/psy-view/', None), - 'psy_reg': ('https://psyplot.github.io/psy-reg/', None), - 'python': ('https://docs.python.org/3/', None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "matplotlib": ("https://matplotlib.org/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master/", None), + "xarray": ("https://xarray.pydata.org/en/stable/", None), + "cartopy": ("https://scitools.org.uk/cartopy/docs/latest/", None), + "psyplot": ("https://psyplot.github.io/psyplot/", None), + "psyplot_gui": ("https://psyplot.github.io/psyplot-gui/", None), + "psy_maps": ("https://psyplot.github.io/psy-maps/", None), + "psy_simple": ("https://psyplot.github.io/psy-simple/", None), + "psy_view": ("https://psyplot.github.io/psy-view/", None), + "psy_reg": ("https://psyplot.github.io/psy-reg/", None), + "python": ("https://docs.python.org/3/", None), } replacements = { - '`psyplot.rcParams`': '`~psyplot.config.rcsetup.rcParams`', - '`psyplot.InteractiveList`': '`~psyplot.data.InteractiveList`', - '`psyplot.InteractiveArray`': '`~psyplot.data.InteractiveArray`', - '`psyplot.open_dataset`': '`~psyplot.data.open_dataset`', - '`psyplot.open_mfdataset`': '`~psyplot.data.open_mfdataset`', - } + "`psyplot.rcParams`": "`~psyplot.config.rcsetup.rcParams`", + "`psyplot.InteractiveList`": "`~psyplot.data.InteractiveList`", + "`psyplot.InteractiveArray`": "`~psyplot.data.InteractiveArray`", + "`psyplot.open_dataset`": "`~psyplot.data.open_dataset`", + "`psyplot.open_mfdataset`": "`~psyplot.data.open_mfdataset`", +} def link_aliases(app, what, name, obj, options, lines): - for (key, val), (i, line) in product(six.iteritems(replacements), - enumerate(lines)): + for (key, val), (i, line) in product( + replacements.items(), enumerate(lines) + ): lines[i] = line.replace(key, val) def setup(app): - app.connect('autodoc-process-docstring', link_aliases) - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} + app.connect("autodoc-process-docstring", link_aliases) diff --git a/psyplot_gui/sphinx_supp/psyplot.rst b/psyplot_gui/sphinx_supp/psyplot.rst index 08456efdf6e9a6a9c5f873636128ea1d7964a4a0..34566865fb1c6e0bfe6462e9b4a1994a49c2328a 100644 --- a/psyplot_gui/sphinx_supp/psyplot.rst +++ b/psyplot_gui/sphinx_supp/psyplot.rst @@ -1,3 +1,7 @@ +.. SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +.. +.. SPDX-License-Identifier: CC-BY-4.0 + =========== Help Window =========== diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..86820bfcd5a3f668f7ba0e5e939e78328ff604da --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,159 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +[build-system] +build-backend = 'setuptools.build_meta' +requires = ['setuptools >= 61.0', 'versioneer[toml]'] + +[project] +name = "psyplot-gui" +dynamic = ["version"] +description = "Graphical user interface for the psyplot package" + +readme = "README.md" +keywords = [ + "visualization", + + "psyplot", + + "netcdf", + + "raster", + + "cartopy", + + "earth-sciences", + + "pyqt", + + "qt", + + "ipython", + + "jupyter", + + "qtconsole", + ] + +authors = [ + { name = 'Philipp S. Sommer', email = 'philipp.sommer@hereon.de' }, +] +maintainers = [ + { name = 'Philipp S. Sommer', email = 'philipp.sommer@hereon.de' }, +] +license = { text = 'LGPL-3.0-only' } + +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Operating System :: OS Independent", +] + +requires-python = '>= 3.9' +dependencies = [ + "psyplot", + # add your dependencies here + "qtconsole", + "fasteners", + "sphinx>=2.4.0", + "sphinx-rtd-theme", +] + +[project.urls] +Homepage = 'https://codebase.helmholtz.cloud/psyplot/psyplot-gui' +Documentation = "https://psyplot.github.io/psyplot-gui" +Source = "https://codebase.helmholtz.cloud/psyplot/psyplot-gui" +Tracker = "https://codebase.helmholtz.cloud/psyplot/psyplot-gui/issues/" + +[project.optional-dependencies] +testsite = [ + "tox", + "isort==5.12.0", + "black==23.1.0", + "blackdoc==0.3.8", + "flake8==6.0.0", + "pre-commit", + "mypy", + "pytest-cov", + "reuse", + "cffconvert", + "netCDF4", + "dask", + "scipy", + "psutil", +] +docs = [ + "autodocsumm", + "sphinx-rtd-theme", + "hereon-netcdf-sphinxext", + "sphinx-design", + "dask", + "netcdf4", + "psy-simple", + "sphinx-argparse", +] +dev = [ + "psyplot-gui[testsite]", + "psyplot-gui[docs]", + "PyYAML", + "types-PyYAML", +] + + +[tool.mypy] +ignore_missing_imports = true + +[tool.setuptools] +zip-safe = false +license-files = ["LICENSES/*"] + +[tool.setuptools.package-data] +psyplot_gui = [ + "psyplot_gui/sphinx_supp/conf.py", + "psyplot_gui/sphinx_supp/psyplot.rst", + "psyplot_gui/sphinx_supp/_static/*", + "psyplot_gui/icons/*.png", + "psyplot_gui/icons/*.png.license", + "psyplot_gui/icons/*.svg", + "psyplot_gui/icons/*.svg.license", +] + +[tool.setuptools.packages.find] +namespaces = false +exclude = [ + 'docs', + 'tests*', + 'examples' +] + +[tool.pytest.ini_options] +addopts = '-v' + +[tool.versioneer] +VCS = 'git' +style = 'pep440' +versionfile_source = 'psyplot_gui/_version.py' +versionfile_build = 'psyplot_gui/_version.py' +tag_prefix = 'v' +parentdir_prefix = 'psyplot-gui-' + +[tool.isort] +profile = "black" +line_length = 79 +src_paths = ["psyplot_gui"] +float_to_top = true +known_first_party = "psyplot_gui" + +[tool.black] +line-length = 79 +target-version = ['py39'] + +[tool.coverage.run] +omit = ["psyplot_gui/_version.py"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 7142a47d24701105d9d50cf42f4878b0c6f9f4c6..0000000000000000000000000000000000000000 --- a/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[versioneer] -VCS = git -style = pep440 -versionfile_source = psyplot_gui/_version.py -versionfile_build = psyplot_gui/_version.py -tag_prefix = v -parentdir_prefix = psyplot-gui- - - diff --git a/setup.py b/setup.py index d1f10da427f55fb10dca4d4c0bd0f9c2eafbd69c..3049cc21c48524b0323f61fca9d3dd3220d58175 100644 --- a/setup.py +++ b/setup.py @@ -1,105 +1,12 @@ -"""Setup script for the psyplot-gui package.""" - -# Disclaimer -# ---------- -# -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH # -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. +# SPDX-License-Identifier: CC0-1.0 -import os.path as osp -from setuptools import setup, find_packages -from setuptools.command.test import test as TestCommand -import sys +"""Setup script for the psyplot-gui package.""" import versioneer +from setuptools import setup - -def readme(): - with open('README.rst') as f: - return f.read() - - -class PyTest(TestCommand): - user_options = [('pytest-args=', 'a', "Arguments to pass to pytest")] - - def initialize_options(self): - TestCommand.initialize_options(self) - self.pytest_args = '' - - def run_tests(self): - import shlex - # import here, cause outside the eggs aren't loaded - import pytest - errno = pytest.main(shlex.split(self.pytest_args)) - sys.exit(errno) - - -cmdclass = versioneer.get_cmdclass({'test': PyTest}) - - -setup(name='psyplot-gui', - version=versioneer.get_version(), - description='Graphical user interface for the psyplot package', - long_description=readme(), - long_description_content_type="text/x-rst", - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'Topic :: Scientific/Engineering :: Visualization', - 'Topic :: Scientific/Engineering :: GIS', - 'Topic :: Scientific/Engineering', - 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Operating System :: OS Independent', - ], - keywords=('visualization netcdf raster cartopy earth-sciences pyqt qt ' - 'ipython jupyter qtconsole'), - url='https://github.com/psyplot/psyplot-gui', - author='Philipp S. Sommer', - author_email='psyplot@hereon.de', - license="LGPL-3.0-only", - packages=find_packages(exclude=['docs', 'tests*', 'examples']), - install_requires=[ - 'psyplot>=1.3.0', - 'qtconsole', - 'fasteners', - 'sphinx>=2.4.0', - 'sphinx_rtd_theme', - ], - package_data={'psyplot_gui': [ - osp.join('psyplot_gui', 'sphinx_supp', 'conf.py'), - osp.join('psyplot_gui', 'sphinx_supp', 'psyplot.rst'), - osp.join('psyplot_gui', 'sphinx_supp', '_static', '*'), - osp.join('psyplot_gui', 'icons', '*.png'), - osp.join('psyplot_gui', 'icons', '*.svg'), - ]}, - project_urls={ - 'Documentation': 'https://psyplot.github.io/psyplot-gui', - 'Source': 'https://github.com/psyplot/psyplot-gui', - 'Tracker': 'https://github.com/psyplot/psyplot-gui/issues', - }, - include_package_data=True, - tests_require=['pytest', 'psutil'], - cmdclass=cmdclass, - zip_safe=False) +setup( + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), +) diff --git a/tests/_base_testing.py b/tests/_base_testing.py index 204cb6500efc6a1acd561930a0e5c6bd2fbcab85..60c8715a644f8223c264f11a430e2c3ceb7aa692 100644 --- a/tests/_base_testing.py +++ b/tests/_base_testing.py @@ -1,43 +1,47 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + # -*- coding: utf-8 -*- """Module defining the base class for the gui test""" import os import os.path as osp import unittest -os.environ['PSYPLOT_PLUGINS'] = ('yes:psyplot_gui_test.plugin::' - 'yes:psy_simple.plugin') - - +from psyplot import rcParams as psy_rcParams from psyplot.config import setup_logging +from psyplot_gui import rcParams +from psyplot_gui.compat.qtcompat import QApplication -test_dir = osp.dirname(__file__) -setup_logging(osp.join(test_dir, 'logging.yml'), env_key='') +os.environ["PSYPLOT_PLUGINS"] = ( + "yes:psyplot_gui_test.plugin::" "yes:psy_simple.plugin" +) -from psyplot_gui.compat.qtcompat import QApplication -from psyplot_gui import rcParams -from psyplot import rcParams as psy_rcParams +test_dir = osp.dirname(__file__) +setup_logging(osp.join(test_dir, "logging.yml"), env_key="") def is_running_in_gui(): from psyplot_gui.main import mainwindow + return mainwindow is not None running_in_gui = is_running_in_gui() -on_travis = os.environ.get('TRAVIS') +on_travis = os.environ.get("TRAVIS") def setup_rcparams(): - rcParams.defaultParams['console.start_channels'][0] = False - rcParams.defaultParams['main.listen_to_port'][0] = False - rcParams.defaultParams['help_explorer.render_docs_parallel'][0] = False - rcParams.defaultParams['help_explorer.use_intersphinx'][0] = False - rcParams.defaultParams['plugins.include'][0] = ['psyplot_gui_test.plugin'] - rcParams.defaultParams['plugins.exclude'][0] = 'all' + rcParams.defaultParams["console.start_channels"][0] = False + rcParams.defaultParams["main.listen_to_port"][0] = False + rcParams.defaultParams["help_explorer.render_docs_parallel"][0] = False + rcParams.defaultParams["help_explorer.use_intersphinx"][0] = False + rcParams.defaultParams["plugins.include"][0] = ["psyplot_gui_test.plugin"] + rcParams.defaultParams["plugins.exclude"][0] = "all" rcParams.update_from_defaultParams() @@ -59,10 +63,12 @@ class PsyPlotGuiTestCase(unittest.TestCase): @classmethod def setUpClass(cls): from psyplot_gui.main import mainwindow + cls._close_app = mainwindow is None cls._app = app if not running_in_gui: import psyplot_gui + psyplot_gui.UNIT_TESTING = True @classmethod @@ -71,6 +77,7 @@ class PsyPlotGuiTestCase(unittest.TestCase): def setUp(self): import psyplot_gui.main as main + if not running_in_gui: setup_rcparams() self.window = main.MainWindow.run(show=False) @@ -78,10 +85,12 @@ class PsyPlotGuiTestCase(unittest.TestCase): self.window = main.mainwindow def tearDown(self): - import psyplot.project as psy import matplotlib.pyplot as plt + import psyplot.project as psy + if not running_in_gui: import psyplot_gui.main as main + self.window.close() rcParams.update_from_defaultParams() psy_rcParams.update_from_defaultParams() @@ -89,8 +98,8 @@ class PsyPlotGuiTestCase(unittest.TestCase): psy_rcParams.disconnect() main._set_mainwindow(None) del self.window - psy.close('all') - plt.close('all') + psy.close("all") + plt.close("all") def get_file(self, fname): """Get the path to the file `fname` diff --git a/tests/conftest.py b/tests/conftest.py index 32489a3dba8a70f386d11a7da476f8f8c13bf97d..4b1139f8532841d7c83be58cdd62c53922873096 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,13 +1,17 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + """Configuration module for running tests with pytest We use a methodology inspired by https://nvbn.github.io/2017/02/02/pytest-leaking/ to show huw many MB are leaked from each test.""" import os -from psutil import Process from collections import namedtuple from itertools import groupby +from psutil import Process _proc = Process(os.getpid()) @@ -18,23 +22,30 @@ def get_consumed_ram(): def pytest_addoption(parser): group = parser.getgroup("psyplot", "psyplot specific options") - group.addoption('--leak-threshold', help="Threshold for leak report", - default=20, type=int) group.addoption( - '--sort-leaks', help="Sort the leaking report in ascending order", - action='store_true') + "--leak-threshold", + help="Threshold for leak report", + default=20, + type=int, + ) + group.addoption( + "--sort-leaks", + help="Sort the leaking report in ascending order", + action="store_true", + ) def pytest_configure(config): global LEAK_LIMIT, SORT_LEAKS - LEAK_LIMIT = config.getoption('leak_threshold') * 1024 * 1024 - SORT_LEAKS = config.getoption('sort_leaks') + LEAK_LIMIT = config.getoption("leak_threshold") * 1024 * 1024 + SORT_LEAKS = config.getoption("sort_leaks") -START = 'START' -END = 'END' -ConsumedRamLogEntry = namedtuple('ConsumedRamLogEntry', - ('nodeid', 'on', 'consumed_ram')) +START = "START" +END = "END" +ConsumedRamLogEntry = namedtuple( + "ConsumedRamLogEntry", ("nodeid", "on", "consumed_ram") +) consumed_ram_log = [] @@ -60,10 +71,14 @@ def pytest_terminal_summary(terminalreporter): for nodeid, (start_entry, end_entry) in grouped: leaked = end_entry.consumed_ram - start_entry.consumed_ram if leaked > LEAK_LIMIT: - leaks.append((leaked // 1024 // 1024, nodeid, - end_entry.consumed_ram // 1024 // 1024)) + leaks.append( + ( + leaked // 1024 // 1024, + nodeid, + end_entry.consumed_ram // 1024 // 1024, + ) + ) if SORT_LEAKS: leaks.sort() for t in leaks: - terminalreporter.write( - 'LEAKED %s MB in %s. Total: %s MB\n' % t) + terminalreporter.write("LEAKED %s MB in %s. Total: %s MB\n" % t) diff --git a/tests/dummy_module.py b/tests/dummy_module.py index 19e7ec1747397e4a982f39031419f9337845b005..526b5e1e0d0e1cd73ab29059250fdf3465f97953 100644 --- a/tests/dummy_module.py +++ b/tests/dummy_module.py @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + """Dummy module Just a dummy module for documentation testing purposes""" diff --git a/tests/logging.yml b/tests/logging.yml index e4c43f98f37f691c4dda89118cf7f616477ad206..a4e2d52c06c6d5d27379ba61bc7672886237e83d 100755 --- a/tests/logging.yml +++ b/tests/logging.yml @@ -1,3 +1,7 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + --- # debug logging settings for the psyplot_gui test suite @@ -71,7 +75,7 @@ loggers: propagate: False level: DEBUG - + psyplot: handlers: [console, debug_file_handler] @@ -79,4 +83,4 @@ loggers: propagate: False level: DEBUG -... \ No newline at end of file +... diff --git a/tests/test-t2m-u-v.nc.license b/tests/test-t2m-u-v.nc.license new file mode 100644 index 0000000000000000000000000000000000000000..5183f0250a91afde79beb27db3d55e30554bf802 --- /dev/null +++ b/tests/test-t2m-u-v.nc.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH + +SPDX-License-Identifier: LGPL-3.0-only diff --git a/tests/test_console.py b/tests/test_console.py index 22dccf912953a9d7e59c5647742f97cd51ab3cd3..2720a8486ab374c8c04e00079386bd14963f6014 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -1,13 +1,18 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + # -*- coding: utf-8 -*- """Skript to test the InProcessShell that is used in the psyplot gui""" +import inspect import re -import six import unittest + import _base_testing as bt import psyplot.project as psy -import inspect -from psyplot_gui.compat.qtcompat import QTest, with_qt5 +import six +from psyplot_gui.compat.qtcompat import QTest, with_qt5 travis_qt_msg = "Does not work on Travis with Qt4" @@ -17,6 +22,7 @@ class ConsoleTest(bt.PsyPlotGuiTestCase): def setUp(self): import psyplot_gui.console + # XXX HACK: Set the _with_prompt attribute to False to tell the # ConsoleWidget to use the _prompt_cursor psyplot_gui.console._with_prompt = False @@ -24,6 +30,7 @@ class ConsoleTest(bt.PsyPlotGuiTestCase): def tearDown(self): import psyplot_gui.console + # XXX HACK: Set the _with_prompt attribute to True again to tell the # ConsoleWidget to use the _prompt_cursor psyplot_gui.console._with_prompt = True @@ -47,66 +54,80 @@ class ConsoleTest(bt.PsyPlotGuiTestCase): test_questionmark, test_bracketleft """ from psyplot_gui.help_explorer import signature + c = self.window.console he = self.window.help_explorer - he.set_viewer('Plain text') + he.set_viewer("Plain text") # we insert the text here otherwise using console _insert_plain_text # method because apparently the text is not inserted when using # QTest.keyClicks - self.insert_text('object') + self.insert_text("object") QTest.keyClicks(c._control, symbol) - sig = '' if six.PY2 else re.sub( - r'^\(\s*self,\s*', '(', str(signature(object.__init__))) + sig = ( + "" + if six.PY2 + else re.sub( + r"^\(\s*self,\s*", "(", str(signature(object.__init__)) + ) + ) header = "object" + sig - bars = '=' * len(header) + bars = "=" * len(header) self.assertEqual( he.viewer.editor.toPlainText(), - '\n'.join([ - bars, header, bars + "\n\n", inspect.getdoc(object), - "\n" + inspect.getdoc(object.__init__)])) + "\n".join( + [ + bars, + header, + bars + "\n\n", + inspect.getdoc(object), + "\n" + inspect.getdoc(object.__init__), + ] + ), + ) @unittest.skipIf(bt.on_travis and not with_qt5, travis_qt_msg) def test_questionmark(self): """Test the connection to the help explorer by typing '?'""" - self._test_object_docu('?') + self._test_object_docu("?") @unittest.skipIf(bt.on_travis and not with_qt5, travis_qt_msg) def test_bracketleft(self): """Test the connection to the help explorer by typing '?'""" - self._test_object_docu('(') + self._test_object_docu("(") @unittest.skipIf(bt.on_travis and not with_qt5, travis_qt_msg) def test_current_object(self): """Test whether the current object is given correctly""" c = self.window.console - self.insert_text('print(test.anything(object') - self.assertEqual(c.get_current_object(True), 'object') + self.insert_text("print(test.anything(object") + self.assertEqual(c.get_current_object(True), "object") try: # qtconsole >4.3 uses the _prompt_cursor attribute cursor = c._prompt_cursor except AttributeError: cursor = c._control.textCursor() curr = cursor.position() - self.insert_text(') + 3') + self.insert_text(") + 3") cursor.setPosition(curr) - self.assertEqual(c.get_current_object(), 'object') + self.assertEqual(c.get_current_object(), "object") def test_command(self): - self.window.console.run_command_in_shell('a = 4') - self.assertEqual(self.window.console.get_obj('a')[1], 4) + self.window.console.run_command_in_shell("a = 4") + self.assertEqual(self.window.console.get_obj("a")[1], 4) def test_mp_sp(self): """Test whether the mp and sp variables are set correctly""" from xarray import DataArray + psy.Project.oncpchange.emit(psy.gcp(True)) psy.Project.oncpchange.emit(psy.gcp()) - self.assertIs(self.window.console.get_obj('mp')[1], psy.gcp(True)) - self.assertIs(self.window.console.get_obj('sp')[1], psy.gcp()) - sp = psy.plot.lineplot(DataArray([1, 2, 3], name='test').to_dataset()) - self.assertIs(self.window.console.get_obj('mp')[1], psy.gcp(True)) - self.assertIs(self.window.console.get_obj('sp')[1], sp) + self.assertIs(self.window.console.get_obj("mp")[1], psy.gcp(True)) + self.assertIs(self.window.console.get_obj("sp")[1], psy.gcp()) + sp = psy.plot.lineplot(DataArray([1, 2, 3], name="test").to_dataset()) + self.assertIs(self.window.console.get_obj("mp")[1], psy.gcp(True)) + self.assertIs(self.window.console.get_obj("sp")[1], sp) sp.close(True, True) - self.assertIs(self.window.console.get_obj('mp')[1], psy.gcp(True)) - self.assertIs(self.window.console.get_obj('sp')[1], psy.gcp()) + self.assertIs(self.window.console.get_obj("mp")[1], psy.gcp(True)) + self.assertIs(self.window.console.get_obj("sp")[1], psy.gcp()) if __name__ == "__main__": diff --git a/tests/test_dataframeeditor.py b/tests/test_dataframeeditor.py index af45f00fce3e2d05e3885cd40bfe3621f0014aef..558c854725d8d28a5efb0adfcda6e31d17fc675c 100644 --- a/tests/test_dataframeeditor.py +++ b/tests/test_dataframeeditor.py @@ -1,14 +1,19 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + # -*- coding: utf-8 -*- """Test module for the :mod:`psyplot_gui.dataframeeditor` module""" -import six import sys -import pandas as pd -import numpy as np -import _base_testing as bt import unittest + +import _base_testing as bt +import numpy as np +import pandas as pd +import six from pandas.testing import assert_frame_equal -from psyplot_gui.compat.qtcompat import Qt, QApplication +from psyplot_gui.compat.qtcompat import QApplication, Qt if six.PY2: try: @@ -40,7 +45,6 @@ def df_equals(df, df_ref, *args, **kwargs): class DataFrameEditorTest(bt.PsyPlotGuiTestCase): - #: The :class:`psyplot_gui.dataframeeditor.DataFrameEditor` editor = None @@ -60,24 +64,25 @@ class DataFrameEditorTest(bt.PsyPlotGuiTestCase): self.editor = None def test_dtypes(self): - df = pd.DataFrame([ - [True, "bool"], - [1+1j, "complex"], - ['test', "string"], - [1.11, "float"], - [1, "int"], - [np.random.rand(3, 3), "Unkown type"], - ["áéÃ", "unicode"], - ], - index=['a', 'b', np.nan, np.nan, np.nan, 'c', - 'd'], - columns=[np.nan, 'Type']) + df = pd.DataFrame( + [ + [True, "bool"], + [1 + 1j, "complex"], + ["test", "string"], + [1.11, "float"], + [1, "int"], + [np.random.rand(3, 3), "Unkown type"], + ["áéÃ", "unicode"], + ], + index=["a", "b", np.nan, np.nan, np.nan, "c", "d"], + columns=[np.nan, "Type"], + ) self.editor.set_df(df) self.assertIs(self.table.model().df, df) def test_multiindex(self): """Test the handling of DataFrames with MultiIndex""" - df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list('abc')) + df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list("abc")) self.editor.set_df(df) self.assertTrue(self.model.index_editable) self.assertTrue(self.editor.cb_index_editable.isChecked()) @@ -85,25 +90,27 @@ class DataFrameEditorTest(bt.PsyPlotGuiTestCase): table = self.table table.selectColumn(1) table.set_index_action.trigger() - self.assertEqual(list(df.index.names), ['a']) + self.assertEqual(list(df.index.names), ["a"]) self.assertTrue(self.model.index_editable) self.assertTrue(self.editor.cb_index_editable.isChecked()) table.selectColumn(2) table.append_index_action.trigger() - self.assertEqual(list(df.index.names), ['a', 'b']) + self.assertEqual(list(df.index.names), ["a", "b"]) self.assertFalse(self.model.index_editable) self.assertFalse(self.editor.cb_index_editable.isChecked()) table.selectColumn(1) table.set_index_action.trigger() - self.assertEqual(list(df.index.names), ['index']) - self.assertEqual(list(df.columns), list('abc')) + self.assertEqual(list(df.index.names), ["index"]) + self.assertEqual(list(df.columns), list("abc")) self.assertTrue(self.model.index_editable) self.assertTrue(self.editor.cb_index_editable.isChecked()) def test_sort(self): """Test the sorting""" - df = pd.DataFrame([[4, 5, 6+1j], [1, object, 3]], columns=list('abc')) + df = pd.DataFrame( + [[4, 5, 6 + 1j], [1, object, 3]], columns=list("abc") + ) self.editor.set_df(df) self.assertTrue(self.model.sort(1, return_check=True)) self.assertEqual(list(df.index.values), [1, 0]) @@ -111,29 +118,33 @@ class DataFrameEditorTest(bt.PsyPlotGuiTestCase): self.assertEqual(list(df.index.values), [0, 1]) # test complex numbers - self.assertTrue(self.model.sort(3, Qt.AscendingOrder, - return_check=True)) - self.assertEqual(list(df['c'].values), [3+0j, 6+1j]) - self.assertTrue(self.model.sort(3, Qt.DescendingOrder, - return_check=True)) - self.assertEqual(list(df['c'].values), [6+1j, 3+0j]) + self.assertTrue( + self.model.sort(3, Qt.AscendingOrder, return_check=True) + ) + self.assertEqual(list(df["c"].values), [3 + 0j, 6 + 1j]) + self.assertTrue( + self.model.sort(3, Qt.DescendingOrder, return_check=True) + ) + self.assertEqual(list(df["c"].values), [6 + 1j, 3 + 0j]) # sorting is not enabled self.table.sortByColumn(1) - self.assertEqual(list(df['a']), [4, 1]) + self.assertEqual(list(df["a"]), [4, 1]) # enable sorting self.table.setSortingEnabled(True) self.table.header_class.setSortIndicator(1, Qt.AscendingOrder) self.table.sortByColumn(1) - self.assertEqual(list(df['a']), [1, 4]) + self.assertEqual(list(df["a"]), [1, 4]) self.table.header_class.setSortIndicator(1, Qt.DescendingOrder) self.table.sortByColumn(1) - self.assertEqual(list(df['a']), [4, 1]) + self.assertEqual(list(df["a"]), [4, 1]) @unittest.expectedFailure def test_sort_failure(self): - df = pd.DataFrame([[4, 5, 6+1j], [1, object, 3]], columns=list('abc')) + df = pd.DataFrame( + [[4, 5, 6 + 1j], [1, object, 3]], columns=list("abc") + ) self.editor.set_df(df) # test false sorting @@ -142,7 +153,9 @@ class DataFrameEditorTest(bt.PsyPlotGuiTestCase): @unittest.expectedFailure def test_sort_failure_2(self): - df = pd.DataFrame([[4, 5, 6+1j], [1, object, 3]], columns=list('abc')) + df = pd.DataFrame( + [[4, 5, 6 + 1j], [1, object, 3]], columns=list("abc") + ) self.editor.set_df(df) # test a column that cannot be sorted @@ -152,7 +165,7 @@ class DataFrameEditorTest(bt.PsyPlotGuiTestCase): def test_edit(self): """Test the editing of the editor""" - df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list('abc')) + df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list("abc")) self.editor.set_df(df) table = self.table @@ -171,8 +184,8 @@ class DataFrameEditorTest(bt.PsyPlotGuiTestCase): # now we change a data type table.selectColumn(2) - table.dtype_actions['To float'].trigger() - self.assertIs(df.dtypes['b'], np.array(5.4).dtype) + table.dtype_actions["To float"].trigger() + self.assertIs(df.dtypes["b"], np.array(5.4).dtype) def test_large_df(self): df = pd.DataFrame(np.zeros((int(1e6), 100))) @@ -184,16 +197,18 @@ class DataFrameEditorTest(bt.PsyPlotGuiTestCase): self.assertTrue(model.can_fetch_more(rows=True, columns=True)) old_rows, old_cols = model.rows_loaded, model.cols_loaded self.table.load_more_data( - self.table.verticalScrollBar().maximum(), rows=True) + self.table.verticalScrollBar().maximum(), rows=True + ) self.table.load_more_data( - self.table.horizontalScrollBar().maximum(), columns=True) + self.table.horizontalScrollBar().maximum(), columns=True + ) self.assertGreater(model.rows_loaded, old_rows) self.assertGreater(model.cols_loaded, old_cols) self.assertLess(model.rows_loaded, df.shape[0]) self.assertLess(model.cols_loaded, df.shape[1]) def test_insert_rows_above(self): - df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list('abc')) + df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list("abc")) self.editor.set_df(df) # insert one row @@ -201,18 +216,20 @@ class DataFrameEditorTest(bt.PsyPlotGuiTestCase): self.table.insert_row_above_action.trigger() self.assertEqual(df.shape, (3, 3)) self.assertEqual(list(df.index), [0, 0, 1]) - self.assertTrue(np.isnan(df.iloc[1, :].values).all(), - msg=str(df.iloc[1, :])) + self.assertTrue( + np.isnan(df.iloc[1, :].values).all(), msg=str(df.iloc[1, :]) + ) # insert two rows self.model.insertRows(2, 2) self.assertEqual(df.shape, (5, 3)) self.assertEqual(list(df.index), [0, 0, 0, 0, 1]) - self.assertTrue(np.isnan(df.iloc[2:-1, :].values).all(), - msg=str(df.iloc[2:-1, :])) + self.assertTrue( + np.isnan(df.iloc[2:-1, :].values).all(), msg=str(df.iloc[2:-1, :]) + ) def test_insert_rows_below(self): - df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list('abc')) + df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list("abc")) self.editor.set_df(df) # insert one row @@ -220,18 +237,20 @@ class DataFrameEditorTest(bt.PsyPlotGuiTestCase): self.table.insert_row_below_action.trigger() self.assertEqual(df.shape, (3, 3)) self.assertEqual(list(df.index), [0, 1, 1]) - self.assertTrue(np.isnan(df.iloc[2, :].values).all(), - msg=str(df.iloc[2, :])) + self.assertTrue( + np.isnan(df.iloc[2, :].values).all(), msg=str(df.iloc[2, :]) + ) # insert two rows self.model.insertRows(3, 2) self.assertEqual(df.shape, (5, 3)) self.assertEqual(list(df.index), [0, 1, 1, 1, 1]) - self.assertTrue(np.isnan(df.iloc[-2:, :].values).all(), - msg=str(df.iloc[-2:, :])) + self.assertTrue( + np.isnan(df.iloc[-2:, :].values).all(), msg=str(df.iloc[-2:, :]) + ) def test_copy(self): - df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list('abc')) + df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list("abc")) self.editor.set_df(df) self.table.selectAll() self.table.copy() @@ -247,23 +266,26 @@ class DataFrameEditorTest(bt.PsyPlotGuiTestCase): arr = np.loadtxt(stream) self.assertEqual(arr.tolist(), [1, 4]) - @unittest.skipIf(sys.platform == 'win32', - 'Avoid potential troubles with temporary csv files.') + @unittest.skipIf( + sys.platform == "win32", + "Avoid potential troubles with temporary csv files.", + ) def test_open_dataframe(self): """Test the opening of a dataframe""" from tempfile import NamedTemporaryFile - df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list('abc')) + + df = pd.DataFrame([[1, 2, 3], [4, 5, 6]], columns=list("abc")) self.editor.open_dataframe(df) - self.editor.open_dataframe('') - f = NamedTemporaryFile(suffix='.csv') + self.editor.open_dataframe("") + f = NamedTemporaryFile(suffix=".csv") df.to_csv(f.name, index=False) self.editor.open_dataframe(f.name) self.assertIsNone(df_equals(self.model.df, df)) @unittest.expectedFailure def test_open_nonexistent(self): - self.editor.open_dataframe(u'NONEXISTENT.csv') - self.assertIsNone(df_equals(self.model.df, df)) + self.editor.open_dataframe("NONEXISTENT.csv") + self.assertIsNone(self.model.df) def test_close(self): self.editor.close() diff --git a/tests/test_dependencies.py b/tests/test_dependencies.py index c62f39fb51bbf6e48157110eeef3e2f5445ef179..d364265f768afeb7e0281250b8cdde8b7578234c 100644 --- a/tests/test_dependencies.py +++ b/tests/test_dependencies.py @@ -1,12 +1,18 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + # -*- coding: utf-8 -*- """Script to test the :mod:`psyplot_gui.dependencies` module""" import unittest -import yaml + import _base_testing as bt -from psyplot_gui.compat.qtcompat import QLabel -import psyplot import numpy as np +import psyplot +import yaml + import psyplot_gui +from psyplot_gui.compat.qtcompat import QLabel class TestDependencies(bt.PsyPlotGuiTestCase): @@ -26,16 +32,16 @@ class TestDependencies(bt.PsyPlotGuiTestCase): def test_widget(self): """Test whether the tree is filled correctly""" deps = self.deps - label = QLabel('', parent=self.window) + label = QLabel("", parent=self.window) deps.tree.selectAll() deps.copy_selected(label) d = yaml.load(str(label.text()), Loader=yaml.Loader) - self.assertEqual(d['psyplot'], psyplot.__version__) - self.assertEqual(d['psyplot_gui'], psyplot_gui.__version__) - self.assertIn('numpy', d) - self.assertEqual(d['numpy'], np.__version__) + self.assertEqual(d["psyplot"], psyplot.__version__) + self.assertEqual(d["psyplot_gui"], psyplot_gui.__version__) + self.assertIn("numpy", d) + self.assertEqual(d["numpy"], np.__version__) label.close() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_fmt_widget.py b/tests/test_fmt_widget.py index 267527f0feb47f6be3a0cad433e26111f3feb395..557e01f57584950f39664bf5919887a330cd8fa2 100644 --- a/tests/test_fmt_widget.py +++ b/tests/test_fmt_widget.py @@ -1,8 +1,19 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + """Test module for the psyplot_gui.fmt_widget module""" -import yaml import _base_testing as bt +import yaml + from psyplot_gui.compat.qtcompat import ( - QTest, Qt, QPushButton, QtCore, QtGui, with_qt5) + QPushButton, + Qt, + QtCore, + QTest, + QtGui, + with_qt5, +) if with_qt5: ClearAndSelect = QtCore.QItemSelectionModel.ClearAndSelect @@ -19,14 +30,17 @@ class FormatoptionWidgetTest(bt.PsyPlotGuiTestCase): def setUp(self): import psyplot.project as psy + super(FormatoptionWidgetTest, self).setUp() self.project = psy.plot.gui_test_plotter( - self.get_file('test-t2m-u-v.nc'), name='t2m') + self.get_file("test-t2m-u-v.nc"), name="t2m" + ) def tearDown(self): import psyplot.project as psy + super(FormatoptionWidgetTest, self).tearDown() - psy.close('all') + psy.close("all") del self.project def test_fmto_groups(self): @@ -34,21 +48,30 @@ class FormatoptionWidgetTest(bt.PsyPlotGuiTestCase): fmt_w = self.fmt_widget # test groups self.assertEqual( - list(map(fmt_w.group_combo.itemText, - range(fmt_w.group_combo.count()))), - ['Dimensions', - 'All formatoptions', - 'Miscallaneous formatoptions', - 'Post processing formatoptions']) + list( + map( + fmt_w.group_combo.itemText, + range(fmt_w.group_combo.count()), + ) + ), + [ + "Dimensions", + "All formatoptions", + "Miscallaneous formatoptions", + "Post processing formatoptions", + ], + ) def test_dims(self): """Test whether the fmto combo for dimensions is filled correctly""" fmt_w = self.fmt_widget # test groups self.assertEqual( - list(map(fmt_w.fmt_combo.itemText, - range(fmt_w.fmt_combo.count()))), - list(self.project[0].psy.base['t2m'].dims)) + list( + map(fmt_w.fmt_combo.itemText, range(fmt_w.fmt_combo.count())) + ), + list(self.project[0].psy.base["t2m"].dims), + ) def test_dim_widget(self): """Test the :class:`psyplot_gui.fmt_widget.DimensionsWidget`""" @@ -61,92 +84,96 @@ class FormatoptionWidgetTest(bt.PsyPlotGuiTestCase): item = model.item(2) selection_model.select(model.indexFromItem(item), ClearAndSelect) fmt_w.dim_widget.insert_from_combo() - self.assertEqual(fmt_w.get_text(), '[1]') + self.assertEqual(fmt_w.get_text(), "[1]") # select a second item item = model.item(3) selection_model.select(model.indexFromItem(item), ClearAndSelect) fmt_w.dim_widget.insert_from_combo() - self.assertEqual(fmt_w.get_text(), '[1, 2]') + self.assertEqual(fmt_w.get_text(), "[1, 2]") # change to single selection fmt_w.dim_widget.set_single_selection(True) fmt_w.dim_widget.insert_from_combo() - self.assertEqual(fmt_w.get_text(), '2') + self.assertEqual(fmt_w.get_text(), "2") def test_fmtos(self): """Test whether the fmto combo for formatoptions is filled correctly""" fmt_w = self.fmt_widget - fmt_w.group_combo.setCurrentIndex(fmt_w.group_combo.findText( - 'Miscallaneous formatoptions')) + fmt_w.group_combo.setCurrentIndex( + fmt_w.group_combo.findText("Miscallaneous formatoptions") + ) # test groups self.assertEqual( - list(map(fmt_w.fmt_combo.itemText, - range(fmt_w.fmt_combo.count()))), - ['Test formatoption (fmt1)', 'Second test formatoption (fmt2)']) + list( + map(fmt_w.fmt_combo.itemText, range(fmt_w.fmt_combo.count())) + ), + ["Test formatoption (fmt1)", "Second test formatoption (fmt2)"], + ) def test_toggle_multiline(self): """Test toggle the multiline text editor""" fmt_w = self.fmt_widget self.assertTrue(fmt_w.line_edit.isVisible()) self.assertFalse(fmt_w.text_edit.isVisible()) - fmt_w.set_obj('test') - self.assertEqual(fmt_w.line_edit.text()[1:-1], 'test') + fmt_w.set_obj("test") + self.assertEqual(fmt_w.line_edit.text()[1:-1], "test") # now toggle the button QTest.mouseClick(fmt_w.multiline_button, Qt.LeftButton) self.assertFalse(fmt_w.line_edit.isVisible()) self.assertTrue(fmt_w.text_edit.isVisible()) - self.assertEqual(fmt_w.text_edit.toPlainText()[1:-1], 'test') - fmt_w.insert_obj('test') - self.assertEqual(fmt_w.text_edit.toPlainText()[1:-1], 'testtest') + self.assertEqual(fmt_w.text_edit.toPlainText()[1:-1], "test") + fmt_w.insert_obj("test") + self.assertEqual(fmt_w.text_edit.toPlainText()[1:-1], "testtest") # and toggle again QTest.mouseClick(fmt_w.multiline_button, Qt.LeftButton) self.assertTrue(fmt_w.line_edit.isVisible()) self.assertFalse(fmt_w.text_edit.isVisible()) - self.assertEqual(fmt_w.line_edit.text()[1:-1], 'testtest') + self.assertEqual(fmt_w.line_edit.text()[1:-1], "testtest") def test_run_code(self): """Test updating the plot""" fmt_w = self.fmt_widget self.assertTrue(fmt_w.yaml_cb.isChecked()) - fmt_w.group_combo.setCurrentIndex(fmt_w.group_combo.findText( - 'Miscallaneous formatoptions')) - fmt_w.set_obj('test') + fmt_w.group_combo.setCurrentIndex( + fmt_w.group_combo.findText("Miscallaneous formatoptions") + ) + fmt_w.set_obj("test") QTest.keyClick(fmt_w.line_edit, Qt.Key_Return) - self.assertEqual(self.project.plotters[0].fmt1.value, 'test') + self.assertEqual(self.project.plotters[0].fmt1.value, "test") # test python code fmt_w.fmt_combo.setCurrentIndex(1) fmt_w.yaml_cb.setChecked(False) fmt_w.set_obj("second test") QTest.mouseClick(fmt_w.run_button, Qt.LeftButton) - self.assertEqual(self.project.plotters[0].fmt2.value, 'second test') + self.assertEqual(self.project.plotters[0].fmt2.value, "second test") def test_fmt_widget(self): - """Test the :meth:`psyplot.plotter.Formatoption.get_fmt_widget` method - """ + """Test the :meth:`psyplot.plotter.Formatoption.get_fmt_widget` method""" fmt_w = self.fmt_widget self.assertIs(fmt_w.fmt_widget, fmt_w.dim_widget) - fmt_w.group_combo.setCurrentIndex(fmt_w.group_combo.findText( - 'Miscallaneous formatoptions')) + fmt_w.group_combo.setCurrentIndex( + fmt_w.group_combo.findText("Miscallaneous formatoptions") + ) self.assertIsInstance(fmt_w.fmt_widget, QPushButton) self.assertFalse(yaml.load(fmt_w.line_edit.text(), Loader=yaml.Loader)) - fmt_w.line_edit.setText('') + fmt_w.line_edit.setText("") QTest.mouseClick(fmt_w.fmt_widget, Qt.LeftButton) - self.assertEqual(fmt_w.line_edit.text()[1:-1], 'Test') + self.assertEqual(fmt_w.line_edit.text()[1:-1], "Test") # test with objects other than string fmt_w.fmt_combo.setCurrentIndex(1) self.assertIsInstance(fmt_w.fmt_widget, QPushButton) fmt_w.clear_text() QTest.mouseClick(fmt_w.fmt_widget, Qt.LeftButton) - self.assertEqual(fmt_w.line_edit.text(), '2') + self.assertEqual(fmt_w.line_edit.text(), "2") # check without yaml fmt_w.yaml_cb.setChecked(False) QTest.mouseClick(fmt_w.fmt_widget, Qt.LeftButton) - self.assertEqual(fmt_w.line_edit.text(), '22') + self.assertEqual(fmt_w.line_edit.text(), "22") def test_get_obj(self): self.fmt_widget.line_edit.setText('{"okay": True}') - self.assertEqual(self.fmt_widget.get_obj(), {'okay': True}) + self.assertEqual(self.fmt_widget.get_obj(), {"okay": True}) diff --git a/tests/test_help_explorer.py b/tests/test_help_explorer.py index 818fcc069ea9146325b34ebeba631d5b9521420b..7c06b597e7b654cfe5a994f3cb0042541f9c0453 100644 --- a/tests/test_help_explorer.py +++ b/tests/test_help_explorer.py @@ -1,14 +1,24 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + """Module for testing the components of the :class:`psyplot_gui.help_explorer.HelpExplorer` class""" -import unittest import inspect import os.path as osp +import unittest + import _base_testing as bt import dummy_module as d + from psyplot_gui import rcParams -from psyplot_gui.compat.qtcompat import QTest, Qt, asstring +from psyplot_gui.compat.qtcompat import Qt, QTest, asstring from psyplot_gui.help_explorer import ( - html2file, UrlHelp, HelpExplorer, _viewers) + HelpExplorer, + UrlHelp, + _viewers, + html2file, +) class UrlHelpTestMixin(bt.PsyPlotGuiTestCase): @@ -17,22 +27,22 @@ class UrlHelpTestMixin(bt.PsyPlotGuiTestCase): @classmethod def setUpClass(cls): super(UrlHelpTestMixin, cls).setUpClass() - cls._original_intersphinx = rcParams['help_explorer.use_intersphinx'] - rcParams['help_explorer.use_intersphinx'] = False + cls._original_intersphinx = rcParams["help_explorer.use_intersphinx"] + rcParams["help_explorer.use_intersphinx"] = False # we render the docs in the same process to avoid problems with # signals not being send and time problems - cls._original_pdocs = rcParams['help_explorer.render_docs_parallel'] - rcParams['help_explorer.render_docs_parallel'] = False + cls._original_pdocs = rcParams["help_explorer.render_docs_parallel"] + rcParams["help_explorer.render_docs_parallel"] = False @classmethod def tearDownClass(cls): super(UrlHelpTestMixin, cls).tearDownClass() - rcParams['help_explorer.use_intersphinx'] = cls._original_intersphinx - rcParams['help_explorer.render_docs_parallel'] = cls._original_pdocs + rcParams["help_explorer.use_intersphinx"] = cls._original_intersphinx + rcParams["help_explorer.render_docs_parallel"] = cls._original_pdocs def setUp(self): super(UrlHelpTestMixin, self).setUp() - self.help_explorer.set_viewer('HTML help') + self.help_explorer.set_viewer("HTML help") @property def help_explorer(self): @@ -41,12 +51,13 @@ class UrlHelpTestMixin(bt.PsyPlotGuiTestCase): @property def viewer(self): ret = self.help_explorer.viewer - self.assertIs(ret, self.help_explorer.viewers['HTML help']) + self.assertIs(ret, self.help_explorer.viewers["HTML help"]) return ret def _test_if_sphinx_worked(self, oname): - html = osp.join(osp.join(self.viewer.sphinx_dir, '_build', - 'html', oname + '.html')) + html = osp.join( + osp.join(self.viewer.sphinx_dir, "_build", "html", oname + ".html") + ) self.assertEqual(html2file(self.viewer.html.url().toString()), html) # we emit the urlChanged signal manually because it is not emitted # without main loop @@ -54,16 +65,17 @@ class UrlHelpTestMixin(bt.PsyPlotGuiTestCase): self.assertEqual(self.viewer.tb_url.currentText(), oname) def _test_browsing(self): - rcParams['help_explorer.online'] = True - self.viewer.browse('www.google.de') + rcParams["help_explorer.online"] = True + self.viewer.browse("www.google.de") url = asstring(self.viewer.html.url().toString()) - self.assertTrue(url.startswith('https://www.google.de'), - msg='Wrong url ' + url) + self.assertTrue( + url.startswith("https://www.google.de"), msg="Wrong url " + url + ) def _test_object_docu(self, obj, oname): """Test whether an html help of a python object can be shown""" self.help_explorer.show_help(obj, oname) - fname = osp.join(self.viewer.sphinx_dir, oname + '.rst') + fname = osp.join(self.viewer.sphinx_dir, oname + ".rst") self.assertTrue(osp.exists(fname), msg=fname + " is not existent!") self._test_if_sphinx_worked(oname) @@ -82,34 +94,36 @@ class UrlHelpTest(UrlHelpTestMixin): ============= Just a dummy string""" - self.help_explorer.show_rst(s, 'test') - fname = osp.join(self.viewer.sphinx_dir, 'test.rst') + self.help_explorer.show_rst(s, "test") + fname = osp.join(self.viewer.sphinx_dir, "test.rst") self.assertTrue(osp.exists(fname), msg=fname + " is not existent!") - self._test_if_sphinx_worked('test') + self._test_if_sphinx_worked("test") def test_module_doc(self): """Test whether the sphinx rendering works for a module""" - self._test_object_docu(d, 'dummy_module') + self._test_object_docu(d, "dummy_module") def test_class_doc(self): """Test whether the sphinx rendering works for a class""" - self._test_object_docu(d.DummyClass, 'd.DummyClass') + self._test_object_docu(d.DummyClass, "d.DummyClass") def test_func_doc(self): """Test whether the sphinx rendering works for a class""" - self._test_object_docu(d.dummy_func, 'd.dummy_func') + self._test_object_docu(d.dummy_func, "d.dummy_func") def test_method_doc(self): """Test whether the sphinx rendering works for a method""" - self._test_object_docu(d.DummyClass.dummy_method, - 'd.DummyClass.dummy_method') + self._test_object_docu( + d.DummyClass.dummy_method, "d.DummyClass.dummy_method" + ) ini = d.DummyClass() - self._test_object_docu(ini.dummy_method, 'ini.dummy_method') + self._test_object_docu(ini.dummy_method, "ini.dummy_method") def test_instance_doc(self): """Test whether the sphinx rendering works for a instance of a class""" ini = d.DummyClass() - self._test_object_docu(ini, 'ini') + self._test_object_docu(ini, "ini") + # XXX Not yet working XXX # def test_classattr_doc(self): @@ -128,7 +142,7 @@ class BrowserTest(UrlHelpTestMixin): def setUp(self): super(BrowserTest, self).setUp() self._help = viewer = UrlHelp(parent=self.window.help_explorer) - self.window.help_explorer.viewers['HTML help'] = viewer + self.window.help_explorer.viewers["HTML help"] = viewer self.window.help_explorer.set_viewer(viewer) def tearDown(self): @@ -137,37 +151,39 @@ class BrowserTest(UrlHelpTestMixin): def test_added_url(self): """Test to add an url on the top""" + def check_google(): - combo.add_text_on_top('https://www.google.com/', block=True) - self.assertEqual(combo.itemText(0), 'https://www.google.com/') + combo.add_text_on_top("https://www.google.com/", block=True) + self.assertEqual(combo.itemText(0), "https://www.google.com/") + combo = self.viewer.tb_url current = combo.itemText(0) check_google() - combo.insertItem(0, 'test') + combo.insertItem(0, "test") check_google() - self.assertEqual(combo.itemText(1), 'test') + self.assertEqual(combo.itemText(1), "test") self.assertEqual(combo.itemText(2), current) def test_lock(self): """Test the url lock""" url = self.viewer.html.url().toString() QTest.mouseClick(self.viewer.bt_lock, Qt.LeftButton) - self.help_explorer.show_help(int, 'int') - fname = osp.join(self.viewer.sphinx_dir, 'int.rst') - self.assertFalse(osp.exists(fname), msg=fname + ' exists wrongly!') - self.help_explorer.show_rst(int.__doc__, 'int') - self.assertFalse(osp.exists(fname), msg=fname + ' exists wrongly!') - self.viewer.browse('www.google.de') + self.help_explorer.show_help(int, "int") + fname = osp.join(self.viewer.sphinx_dir, "int.rst") + self.assertFalse(osp.exists(fname), msg=fname + " exists wrongly!") + self.help_explorer.show_rst(int.__doc__, "int") + self.assertFalse(osp.exists(fname), msg=fname + " exists wrongly!") + self.viewer.browse("www.google.de") self.assertEqual(self.viewer.html.url().toString(), url) def test_url_lock(self): """Test whether to object documentation works""" self._test_browsing() QTest.mouseClick(self.viewer.bt_url_lock, Qt.LeftButton) - self.help_explorer.show_help(int, 'int') - self._test_object_docu(int, 'int') - self.viewer.browse('www.unil.ch') - self._test_object_docu(int, 'int') + self.help_explorer.show_help(int, "int") + self._test_object_docu(int, "int") + self.viewer.browse("www.unil.ch") + self._test_object_docu(int, "int") class TextHelpTest(bt.PsyPlotGuiTestCase): @@ -175,7 +191,7 @@ class TextHelpTest(bt.PsyPlotGuiTestCase): def setUp(self): super(TextHelpTest, self).setUp() - self.help_explorer.set_viewer('Plain text') + self.help_explorer.set_viewer("Plain text") @property def help_explorer(self): @@ -193,8 +209,9 @@ class TextHelpTest(bt.PsyPlotGuiTestCase): May be improved in the future for a more exact test""" self.assertTrue( doc in self.viewer.editor.toPlainText(), - msg="%s was not documented!\nObject docu: %s\nHelp test: %s" % ( - oname, doc, self.viewer.editor.toPlainText())) + msg="%s was not documented!\nObject docu: %s\nHelp test: %s" + % (oname, doc, self.viewer.editor.toPlainText()), + ) def _test_object_docu(self, obj, oname, doc=None): """Test whether an help of a python object can be shown""" @@ -210,32 +227,33 @@ class TextHelpTest(bt.PsyPlotGuiTestCase): ============= Just a dummy string""" - self.help_explorer.show_rst(s, 'test') - self._test_doc(s, 'test') + self.help_explorer.show_rst(s, "test") + self._test_doc(s, "test") def test_module_doc(self): """Test whether the sphinx rendering works for a module""" - self._test_object_docu(d, 'dummy_module') + self._test_object_docu(d, "dummy_module") def test_class_doc(self): """Test whether the sphinx rendering works for a class""" - self._test_object_docu(d.DummyClass, 'd.DummyClass') + self._test_object_docu(d.DummyClass, "d.DummyClass") def test_func_doc(self): """Test whether the sphinx rendering works for a class""" - self._test_object_docu(d.dummy_func, 'd.dummy_func') + self._test_object_docu(d.dummy_func, "d.dummy_func") def test_method_doc(self): """Test whether the sphinx rendering works for a method""" - self._test_object_docu(d.DummyClass.dummy_method, - 'd.DummyClass.dummy_method') + self._test_object_docu( + d.DummyClass.dummy_method, "d.DummyClass.dummy_method" + ) ini = d.DummyClass() - self._test_object_docu(ini.dummy_method, 'ini.dummy_method') + self._test_object_docu(ini.dummy_method, "ini.dummy_method") def test_instance_doc(self): """Test whether the sphinx rendering works for a instance of a class""" ini = d.DummyClass() - self._test_object_docu(ini, 'ini') + self._test_object_docu(ini, "ini") class NoHTMLTest(TextHelpTest): @@ -244,8 +262,8 @@ class NoHTMLTest(TextHelpTest): @classmethod def setUpClass(cls): super().setUpClass() - rcParams['help_explorer.use_webengineview'] = False - del HelpExplorer.viewers['HTML help'] + rcParams["help_explorer.use_webengineview"] = False + del HelpExplorer.viewers["HTML help"] cls._orig_viewers = _viewers.copy() _viewers.clear() @@ -254,16 +272,16 @@ class NoHTMLTest(TextHelpTest): @classmethod def tearDownClass(cls): - rcParams['help_explorer.use_webengineview'] = True - HelpExplorer.viewers['HTML help'] = UrlHelp + rcParams["help_explorer.use_webengineview"] = True + HelpExplorer.viewers["HTML help"] = UrlHelp for key, val in cls._orig_viewers.items(): _viewers[key] = val super().tearDownClass() def test_no_html(self): """Test if the HTML help has been removed""" - self.assertNotIn('HTML help', self.help_explorer.viewers) + self.assertNotIn("HTML help", self.help_explorer.viewers) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_main.py b/tests/test_main.py index 1623cd1608d5076f7ae5c1fd98429c6d95b0de4a..cd09faea3b6aee0ed2a5af46216e9acada864440 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,24 +1,31 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + """Test utilities for the :mod:`psyplot_gui.main` module """ import unittest + import _base_testing as bt class TestMainWindow(bt.PsyPlotGuiTestCase): - def test_plugin(self): from psyplot_gui.main import mainwindow + try: from psyplot_gui_test.plugin import W1, W2 except ImportError: self.skipTest("Test plugin not installed") - self.assertIn('psyplot_gui_test.plugin:W1:w1', mainwindow.plugins) - self.assertIn('psyplot_gui_test.plugin:W2:w2', mainwindow.plugins) + self.assertIn("psyplot_gui_test.plugin:W1:w1", mainwindow.plugins) + self.assertIn("psyplot_gui_test.plugin:W2:w2", mainwindow.plugins) self.assertIsInstance( - mainwindow.plugins['psyplot_gui_test.plugin:W1:w1'], W1) + mainwindow.plugins["psyplot_gui_test.plugin:W1:w1"], W1 + ) self.assertIsInstance( - mainwindow.plugins['psyplot_gui_test.plugin:W2:w2'], W2) - w = mainwindow.plugins['psyplot_gui_test.plugin:W2:w2'] + mainwindow.plugins["psyplot_gui_test.plugin:W2:w2"], W2 + ) + w = mainwindow.plugins["psyplot_gui_test.plugin:W2:w2"] a = w.dock.toggleViewAction() # the plugin should be hidden self.assertFalse(a.isChecked()) @@ -27,14 +34,14 @@ class TestMainWindow(bt.PsyPlotGuiTestCase): def test_central_widget(self): """Test changing the central widget""" - self.window.set_central_widget('help_explorer') + self.window.set_central_widget("help_explorer") self.assertIs(self.window.centralWidget(), self.window.help_explorer) self.window.set_central_widget(self.window.figures_tree) self.assertIs(self.window.centralWidget(), self.window.figures_tree) def test_remove_plugin(self): - self.window.plugins['psyplot_gui_test.plugin:W1:w1'].remove_plugin() - self.assertNotIn('psyplot_gui_test.plugin:W1:w1', self.window.plugins) + self.window.plugins["psyplot_gui_test.plugin:W1:w1"].remove_plugin() + self.assertNotIn("psyplot_gui_test.plugin:W1:w1", self.window.plugins) if __name__ == "__main__": diff --git a/tests/test_plot_creator.py b/tests/test_plot_creator.py index 18f4ddca12e867850b4f3792192cc7e74429bbaf..e5adc10a724ce1bec813956d4ff942caa7ad5912 100644 --- a/tests/test_plot_creator.py +++ b/tests/test_plot_creator.py @@ -1,15 +1,27 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + # -*- coding: utf-8 -*- """Skript to test the InProcessShell that is used in the psyplot gui""" import sys -import six import unittest from itertools import chain + import _base_testing as bt import psyplot.project as psy -from psyplot.compat.pycompat import range +import six + from psyplot_gui.compat.qtcompat import ( - QTest, Qt, QStyleOptionViewItem, QWidget, QValidator, QtGui, QtCore, - asstring) + QStyleOptionViewItem, + Qt, + QtCore, + QTest, + QtGui, + QValidator, + QWidget, + asstring, +) def get_col_num(ax): @@ -35,7 +47,7 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): self.pc = self.window.plot_creator def tearDown(self): - if getattr(self.pc, 'ds', None) is not None: + if getattr(self.pc, "ds", None) is not None: self.pc.ds.close() # make sure the plot creator is closed completely self.pc.close() @@ -44,33 +56,38 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): def test_load_external_file(self): """Test whether an external netCDF file can be loaded""" - fname = self.get_file('test-t2m-u-v.nc') + fname = self.get_file("test-t2m-u-v.nc") self.pc.open_dataset([fname]) vtab = self.pc.variables_table ds = psy.open_dataset(fname) self.assertIn(fname, self.pc.ds_combo.currentText()) self.assertEqual( - {asstring(vtab.item(irow, 0).text()) for irow in range( - vtab.rowCount())}, - set(ds.variables) - set(ds.coords)) + { + asstring(vtab.item(irow, 0).text()) + for irow in range(vtab.rowCount()) + }, + set(ds.variables) - set(ds.coords), + ) ds.close() def test_load_from_console(self): """Test whether a dataset can be loaded that is defined in the console""" - fname = self.get_file('test-t2m-u-v.nc') - if sys.platform == 'win32': - fname = fname.replace('\\', '\\\\') - self.window.console.execute( - "ds = psy.open_dataset('%s')" % fname) + fname = self.get_file("test-t2m-u-v.nc") + if sys.platform == "win32": + fname = fname.replace("\\", "\\\\") + self.window.console.execute("ds = psy.open_dataset('%s')" % fname) vtab = self.pc.variables_table - ds = psy.open_dataset(self.get_file('test-t2m-u-v.nc')) - self.pc.bt_get_ds.get_from_shell('ds') - self.assertIn('ds', self.pc.ds_combo.currentText()) + ds = psy.open_dataset(self.get_file("test-t2m-u-v.nc")) + self.pc.bt_get_ds.get_from_shell("ds") + self.assertIn("ds", self.pc.ds_combo.currentText()) self.assertEqual( - {asstring(vtab.item(irow, 0).text()) for irow in range( - vtab.rowCount())}, - set(ds.variables) - set(ds.coords)) + { + asstring(vtab.item(irow, 0).text()) + for irow in range(vtab.rowCount()) + }, + set(ds.variables) - set(ds.coords), + ) ds.close() self.window.console.execute("ds.close()") @@ -82,10 +99,15 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): atab = self.pc.array_table vtab = self.pc.variables_table self.assertEqual( - [asstring(atab.item(irow, 0).text()) for irow in range( - atab.rowCount())], - [asstring(vtab.item(irow, 0).text()) for irow in range( - vtab.rowCount())]) + [ + asstring(atab.item(irow, 0).text()) + for irow in range(atab.rowCount()) + ], + [ + asstring(vtab.item(irow, 0).text()) + for irow in range(vtab.rowCount()) + ], + ) def test_minusminus(self): """Test the remove all button""" @@ -104,9 +126,12 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): vtab.item(row, 0).setSelected(True) QTest.mouseClick(self.pc.bt_add, Qt.LeftButton) self.assertEqual( - [asstring(atab.item(irow, 0).text()) for irow in range( - atab.rowCount())], - [asstring(vtab.item(irow, 0).text()) for irow in rows]) + [ + asstring(atab.item(irow, 0).text()) + for irow in range(atab.rowCount()) + ], + [asstring(vtab.item(irow, 0).text()) for irow in rows], + ) def test_minus(self): """Test the minus button""" @@ -118,82 +143,98 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): for row in rows: atab.item(row, 0).setSelected(True) QTest.mouseClick(self.pc.bt_remove, Qt.LeftButton) - variables = [asstring(vtab.item(row, 0).text()) - for row in range(vtab.rowCount()) if row not in rows] + variables = [ + asstring(vtab.item(row, 0).text()) + for row in range(vtab.rowCount()) + if row not in rows + ] self.assertEqual( - [asstring(atab.item(irow, 0).text()) for irow in range( - atab.rowCount())], - variables) + [ + asstring(atab.item(irow, 0).text()) + for irow in range(atab.rowCount()) + ], + variables, + ) def test_update_with_dims(self): """Test the update with the given dimensions""" self.test_plusplus() atab = self.pc.array_table atab.selectAll() - atab.update_selected(dims={'time': '3'}) - icol = len(atab.desc_cols) + atab.dims.index('time') - vars3d = {var for var, varo in atab.get_ds().variables.items() - if 'time' in varo.dims} + atab.update_selected(dims={"time": "3"}) + icol = len(atab.desc_cols) + atab.dims.index("time") + vars3d = { + var + for var, varo in atab.get_ds().variables.items() + if "time" in varo.dims + } for irow in range(atab.rowCount()): vname = atab.item(irow, atab.var_col).text() if vname in vars3d: item = atab.item(irow, icol) self.assertEqual( - item.text(), '3', - msg='Wrong time value %s in row %s' % ( - item.text(), irow)) + item.text(), + "3", + msg="Wrong time value %s in row %s" % (item.text(), irow), + ) def test_add_subplots(self): """Test the add subplots button""" from math import ceil + import matplotlib.pyplot as plt + self.test_load_external_file() self.test_plusplus() - self.pc.cols_axis_edit.setText('2') - self.pc.rows_axis_edit.setText('2') - self.pc.max_axis_edit.setText('3') + self.pc.cols_axis_edit.setText("2") + self.pc.rows_axis_edit.setText("2") + self.pc.max_axis_edit.setText("3") QTest.mouseClick(self.pc.bt_add_axes, Qt.LeftButton) nvar = self.pc.array_table.rowCount() - nfigs = int(ceil(nvar / 3.)) + nfigs = int(ceil(nvar / 3.0)) # create the subplots axes = self.pc.array_table.axes - self.assertEqual([ax.numCols for ax in axes], [2] * nvar) - self.assertEqual([ax.numRows for ax in axes], [2] * nvar) + self.assertEqual([ax.get_gridspec().ncols for ax in axes], [2] * nvar) + self.assertEqual([ax.get_gridspec().nrows for ax in axes], [2] * nvar) rows = [0, 0, 1] * nfigs cols = [0, 1, 0] * nfigs self.assertEqual([get_row_num(ax) for ax in axes], rows) self.assertEqual([get_col_num(ax) for ax in axes], cols) fig_nums = list(chain(*([i] * 3 for i in range(1, nfigs + 1)))) self.assertEqual([ax.get_figure().number for ax in axes], fig_nums) - plt.close('all') + plt.close("all") def test_add_single_subplots(self): """Test the add single subplot button""" import matplotlib.pyplot as plt + self.test_load_external_file() self.test_plusplus() - self.pc.cols_axis_edit.setText('2') - self.pc.rows_axis_edit.setText('2') - self.pc.row_axis_edit.setText('1') - self.pc.col_axis_edit.setText('2') + self.pc.cols_axis_edit.setText("2") + self.pc.rows_axis_edit.setText("2") + self.pc.row_axis_edit.setText("1") + self.pc.col_axis_edit.setText("2") self.pc.array_table.selectAll() QTest.mouseClick(self.pc.bt_add_single_axes, Qt.LeftButton) nvar = self.pc.array_table.rowCount() # create the subplots axes = self.pc.array_table.axes # test rows, cols and figure numbers - self.assertEqual([ax.numCols for ax in axes], [2] * nvar) - self.assertEqual([ax.numRows for ax in axes], [2] * nvar) + self.assertEqual([ax.get_gridspec().ncols for ax in axes], [2] * nvar) + self.assertEqual([ax.get_gridspec().nrows for ax in axes], [2] * nvar) self.assertEqual([get_row_num(ax) for ax in axes], [0] * nvar) self.assertEqual([get_col_num(ax) for ax in axes], [1] * nvar) - self.assertEqual([ax.get_figure().number for ax in axes], list( - range(1, nvar + 1))) - plt.close('all') + self.assertEqual( + [ax.get_figure().number for ax in axes], list(range(1, nvar + 1)) + ) + plt.close("all") def test_axescreator_subplots(self): """Test the :class:`psyplot_gui.plot_creator.SubplotCreator`""" import matplotlib.pyplot as plt + from psyplot_gui.plot_creator import AxesCreatorCollection + # load dataset self.test_load_external_file() # create arrays @@ -202,32 +243,35 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): atab = self.pc.array_table items = [atab.item(i, atab.axes_col) for i in range(atab.rowCount())] # create the widget to select the subplots - ac = AxesCreatorCollection('subplot') + ac = AxesCreatorCollection("subplot") w = ac.tb.currentWidget() - w.fig_edit.setText('') - w.cols_edit.setText('2') - w.rows_edit.setText('2') - w.num1_edit.setText('2') - w.num2_edit.setText('2') + w.fig_edit.setText("") + w.cols_edit.setText("2") + w.rows_edit.setText("2") + w.num1_edit.setText("2") + w.num2_edit.setText("2") ac.okpressed.connect(lambda it: atab._change_axes(items, it)) QTest.mouseClick(ac.bt_ok, Qt.LeftButton) nvar = self.pc.array_table.rowCount() # create the subplots axes = self.pc.array_table.axes # test rows, cols and figure numbers - self.assertEqual([ax.numCols for ax in axes], [2] * nvar) - self.assertEqual([ax.numRows for ax in axes], [2] * nvar) + self.assertEqual([ax.get_gridspec().ncols for ax in axes], [2] * nvar) + self.assertEqual([ax.get_gridspec().nrows for ax in axes], [2] * nvar) self.assertEqual([get_row_num(ax) for ax in axes], [0] * nvar) self.assertEqual([get_col_num(ax) for ax in axes], [1] * nvar) - self.assertEqual([ax.get_figure().number for ax in axes], list( - range(1, nvar + 1))) + self.assertEqual( + [ax.get_figure().number for ax in axes], list(range(1, nvar + 1)) + ) # close figures - plt.close('all') + plt.close("all") def test_axescreator_axes(self): """Test the :class:`psyplot_gui.plot_creator.AxesCreator`""" import matplotlib.pyplot as plt + from psyplot_gui.plot_creator import AxesCreatorCollection + # load dataset self.test_load_external_file() # create arrays @@ -236,13 +280,13 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): atab = self.pc.array_table items = [atab.item(i, atab.axes_col) for i in range(atab.rowCount())] # create the widget to select the subplots - ac = AxesCreatorCollection('axes') + ac = AxesCreatorCollection("axes") w = ac.tb.currentWidget() - w.fig_edit.setText('') - w.x0_edit.setText('0.3') - w.y0_edit.setText('0.4') - w.x1_edit.setText('0.7') - w.y1_edit.setText('0.8') + w.fig_edit.setText("") + w.x0_edit.setText("0.3") + w.y0_edit.setText("0.4") + w.x1_edit.setText("0.7") + w.y1_edit.setText("0.8") ac.okpressed.connect(lambda it: atab._change_axes(items, it)) QTest.mouseClick(ac.bt_ok, Qt.LeftButton) nvar = self.pc.array_table.rowCount() @@ -254,17 +298,20 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): self.assertEqual([box.y0 for box in boxes], [0.4] * nvar) self.assertEqual([box.x1 for box in boxes], [0.7] * nvar) self.assertEqual([box.y1 for box in boxes], [0.8] * nvar) - self.assertEqual([ax.get_figure().number for ax in axes], list( - range(1, nvar + 1))) + self.assertEqual( + [ax.get_figure().number for ax in axes], list(range(1, nvar + 1)) + ) # close figures - plt.close('all') + plt.close("all") def test_axescreator_select(self): """Test the :class:`psyplot_gui.plot_creator.AxesSelector`""" import matplotlib.pyplot as plt import numpy as np from matplotlib.backend_bases import MouseEvent, PickEvent + from psyplot_gui.plot_creator import AxesCreatorCollection + # load dataset self.test_load_external_file() # create arrays @@ -276,18 +323,22 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): ax1 = plt.axes([0.3, 0.4, 0.6, 0.3]) plt.figure() ax2 = plt.subplot(211) - ac = AxesCreatorCollection('choose') + ac = AxesCreatorCollection("choose") w = ac.tb.currentWidget() fig = ax1.get_figure() mouseevent1 = MouseEvent( - 'button_release_event', fig.canvas, - *np.mean(ax1.get_position().get_points().T, axis=1)) - w.get_picked_ax(PickEvent('pick', fig.canvas, mouseevent1, artist=ax1)) + "button_release_event", + fig.canvas, + *np.mean(ax1.get_position().get_points().T, axis=1), + ) + w.get_picked_ax(PickEvent("pick", fig.canvas, mouseevent1, artist=ax1)) fig = ax2.get_figure() mouseevent2 = MouseEvent( - 'button_release_event', ax2.get_figure().canvas, - *np.mean(ax2.get_position().get_points().T, axis=1)) - w.get_picked_ax(PickEvent('pick', fig.canvas, mouseevent2, artist=ax2)) + "button_release_event", + ax2.get_figure().canvas, + *np.mean(ax2.get_position().get_points().T, axis=1), + ) + w.get_picked_ax(PickEvent("pick", fig.canvas, mouseevent2, artist=ax2)) ac.okpressed.connect(lambda it: atab._change_axes(items, it)) QTest.mouseClick(ac.bt_ok, Qt.LeftButton) @@ -297,12 +348,12 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): self.assertIs(axes[0], ax1) self.assertIs(axes[1], ax2) # close figures - plt.close('all') + plt.close("all") def test_arrayname_validator(self): """Test the :class:`psyplot_gui.plot_creator.ArrayNameValidator`""" # open dataset - fname = self.get_file('test-t2m-u-v.nc') + fname = self.get_file("test-t2m-u-v.nc") ds = psy.open_dataset(fname) self.pc.bt_get_ds.get_from_shell(ds) @@ -324,10 +375,13 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): validator = editor.validator() # check validation - self.assertEqual(validator.validate(names[1], len(names[1]))[0], - validator.Intermediate) - self.assertEqual(validator.validate('dummy', 5)[0], - validator.Acceptable) + self.assertEqual( + validator.validate(names[1], len(names[1]))[0], + validator.Intermediate, + ) + self.assertEqual( + validator.validate("dummy", 5)[0], validator.Acceptable + ) self.assertNotIn(validator.fixup(names[1]), names) ds.close() @@ -338,7 +392,7 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): from psyplot_gui.compat.qtcompat import QString except ImportError: QString = six.text_type - fname = self.get_file('test-t2m-u-v.nc') + fname = self.get_file("test-t2m-u-v.nc") ds = psy.open_dataset(fname) self.pc.bt_get_ds.get_from_shell(ds) @@ -360,20 +414,27 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): validator = editor.validator() # check validation - self.assertEqual(validator.validate(QString('dummy'), 5)[0], - QValidator.Invalid) - self.assertEqual(validator.validate(QString(names[0]), - len(names[0]))[0], - QValidator.Acceptable) - self.assertEqual(validator.validate(QString(names[0])[:2], 2)[0], - QValidator.Intermediate) + self.assertEqual( + validator.validate(QString("dummy"), 5)[0], QValidator.Invalid + ) + self.assertEqual( + validator.validate(QString(names[0]), len(names[0]))[0], + QValidator.Acceptable, + ) + self.assertEqual( + validator.validate(QString(names[0])[:2], 2)[0], + QValidator.Intermediate, + ) s = atab.sep.join(names) - self.assertEqual(validator.validate(QString(s), len(s))[0], - QValidator.Acceptable) self.assertEqual( - validator.validate( - QString(s[:3] + 'dummy' + s[3:]), len(s) + 5)[0], - QValidator.Invalid) + validator.validate(QString(s), len(s))[0], QValidator.Acceptable + ) + self.assertEqual( + validator.validate(QString(s[:3] + "dummy" + s[3:]), len(s) + 5)[ + 0 + ], + QValidator.Invalid, + ) ds.close() def test_drag_drop(self): @@ -384,11 +445,17 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): # the event! point = QtCore.QPoint(0, 0) data = QtCore.QMimeData() - event = QtGui.QDropEvent(point, Qt.MoveAction, data, Qt.LeftButton, - Qt.NoModifier, QtCore.QEvent.Drop) + event = QtGui.QDropEvent( + point, + Qt.MoveAction, + data, + Qt.LeftButton, + Qt.NoModifier, + QtCore.QEvent.Drop, + ) # open dataset - fname = self.get_file('test-t2m-u-v.nc') + fname = self.get_file("test-t2m-u-v.nc") ds = psy.open_dataset(fname) self.pc.bt_get_ds.get_from_shell(ds) @@ -401,10 +468,13 @@ class PlotCreatorTest(bt.PsyPlotGuiTestCase): atab.selectRow(2) atab.dropOn(event) resorted = [old[i] for i in [2, 0, 1] + list(range(3, len(old)))] - self.assertEqual(list(atab.arr_names_dict.items()), resorted, - msg="Rows not moved correctly!") + self.assertEqual( + list(atab.arr_names_dict.items()), + resorted, + msg="Rows not moved correctly!", + ) ds.close() -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_plugin/psyplot_gui_test/plotter.py b/tests/test_plugin/psyplot_gui_test/plotter.py index 8217e4a38af95131fe7e863b9760963edc281e7a..7c852bd913707494234911cc36b550afdad163d7 100644 --- a/tests/test_plugin/psyplot_gui_test/plotter.py +++ b/tests/test_plugin/psyplot_gui_test/plotter.py @@ -1,8 +1,12 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + # Test module that defines a plotter # # The plotter in this module has been registered by the rcParams in the plugin # package -from psyplot.plotter import Plotter, Formatoption +from psyplot.plotter import Formatoption, Plotter class TestFmt(Formatoption): @@ -10,13 +14,13 @@ class TestFmt(Formatoption): default = None - name = 'Test formatoption' + name = "Test formatoption" def get_fmt_widget(self, parent, project): - """Get the formatoption widget to update this formatoption in the GUI - """ + """Get the formatoption widget to update this formatoption in the GUI""" from psyplot_gui.compat.qtcompat import QPushButton - button = QPushButton('Test', parent) + + button = QPushButton("Test", parent) button.clicked.connect(lambda: parent.insert_obj(button.text())) return button @@ -27,18 +31,17 @@ class TestFmt(Formatoption): class TestFmt2(TestFmt): """Another formatoption to the different types of get_fmt_widget""" - name = 'Second test formatoption' + name = "Second test formatoption" def get_fmt_widget(self, parent, project): - """Get the formatoption widget to update this formatoption in the GUI - """ + """Get the formatoption widget to update this formatoption in the GUI""" from psyplot_gui.compat.qtcompat import QPushButton - button = QPushButton('Test', parent) + + button = QPushButton("Test", parent) button.clicked.connect(lambda: parent.insert_obj(2)) return button class TestPlotter(Plotter): - - fmt1 = TestFmt('fmt1') - fmt2 = TestFmt2('fmt2') + fmt1 = TestFmt("fmt1") + fmt2 = TestFmt2("fmt2") diff --git a/tests/test_plugin/psyplot_gui_test/plugin.py b/tests/test_plugin/psyplot_gui_test/plugin.py index daa2e4472eccda26cccd5dd4ccf0a0088de46b6a..9801e1c1ed8b52e7a19b30c813be885ce9061aba 100644 --- a/tests/test_plugin/psyplot_gui_test/plugin.py +++ b/tests/test_plugin/psyplot_gui_test/plugin.py @@ -1,25 +1,37 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + from psyplot.config.rcsetup import RcParams, validate_bool, validate_dict + from psyplot_gui.common import DockMixin -from psyplot_gui.compat.qtcompat import QWidget, Qt +from psyplot_gui.compat.qtcompat import Qt, QWidget class W1(QWidget, DockMixin): - title = 'w1' + title = "w1" dock_position = Qt.LeftDockWidgetArea class W2(QWidget, DockMixin): - title = 'w2' + title = "w2" dock_position = Qt.BottomDockWidgetArea hidden = True rcParams = RcParams( defaultParams={ - 'test_plugin': [True, validate_bool], - 'project.plotters': [ - {'gui_test_plotter': { - 'module': 'psyplot_gui_test.plotter', - 'plotter_name': 'TestPlotter', 'import_plotter': True}}, - validate_dict]}) + "test_plugin": [True, validate_bool], + "project.plotters": [ + { + "gui_test_plotter": { + "module": "psyplot_gui_test.plotter", + "plotter_name": "TestPlotter", + "import_plotter": True, + } + }, + validate_dict, + ], + } +) rcParams.update_from_defaultParams() diff --git a/tests/test_plugin/setup.py b/tests/test_plugin/setup.py index 9119cccda575a2be91d0682e2d7fd39e50cdf42b..7fec53e2118bfa2a715c5e0810ef789a21dd348a 100644 --- a/tests/test_plugin/setup.py +++ b/tests/test_plugin/setup.py @@ -1,11 +1,20 @@ -from setuptools import setup, find_packages +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only +from setuptools import find_packages, setup -setup(name='psyplot_gui_test', - version='1.0.0', - license="GPLv2", - packages=find_packages(exclude=['docs', 'tests*', 'examples']), - entry_points={'psyplot_gui': ['w1=psyplot_gui_test.plugin:W1', - 'w2=psyplot_gui_test.plugin:W2'], - 'psyplot': ['plugin=psyplot_gui_test.plugin']}, - zip_safe=False) +setup( + name="psyplot_gui_test", + version="1.0.0", + license="GPLv2", + packages=find_packages(exclude=["docs", "tests*", "examples"]), + entry_points={ + "psyplot_gui": [ + "w1=psyplot_gui_test.plugin:W1", + "w2=psyplot_gui_test.plugin:W2", + ], + "psyplot": ["plugin=psyplot_gui_test.plugin"], + }, + zip_safe=False, +) diff --git a/tests/test_preferences.py b/tests/test_preferences.py index cc42fa7a8f4fa224e03e084ad2e841a2eed10cff..27fc4e8ebb648c7a4b4863397e657e99721dbce4 100644 --- a/tests/test_preferences.py +++ b/tests/test_preferences.py @@ -1,18 +1,24 @@ +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + # -*- coding: utf-8 -*- """Script to test the :mod:`psyplot_gui.preferences` module""" import os import os.path as osp import shutil -import unittest -import yaml import tempfile +import unittest from itertools import islice + import _base_testing as bt -from psyplot_gui.compat.qtcompat import QTest, Qt, QDialogButtonBox, asstring -from psyplot_gui import rcParams as gui_rcParams +import yaml from psyplot import rcParams as psy_rcParams -from psyplot_gui.config.rcsetup import GuiRcParams + import psyplot_gui.preferences as prefs +from psyplot_gui import rcParams as gui_rcParams +from psyplot_gui.compat.qtcompat import QDialogButtonBox, Qt, QTest, asstring +from psyplot_gui.config.rcsetup import GuiRcParams class TestRcParamsWidget(bt.PsyPlotGuiTestCase): @@ -51,29 +57,30 @@ class TestRcParamsWidget(bt.PsyPlotGuiTestCase): key = asstring(item.text(0)) s_val = asstring(w.tree.itemWidget(item.child(0), 2).toPlainText()) val = yaml.load(s_val, Loader=yaml.Loader) - self.assertEqual(val, gui_rcParams[key], - msg='Failed item %s: %s' % (key, s_val)) + self.assertEqual( + val, gui_rcParams[key], msg="Failed item %s: %s" % (key, s_val) + ) def test_changing(self): """Test whether the changes are displayed correctly""" w = prefs.GuiRcParamsWidget() - gui_rcParams['console.auto_set_mp'] = True + gui_rcParams["console.auto_set_mp"] = True w.initialize() items = list(w.tree.top_level_items) for item in items: - if item.text(0) == 'console.auto_set_mp': + if item.text(0) == "console.auto_set_mp": iw = w.tree.itemWidget(item.child(0), 2) - iw.setPlainText('f') + iw.setPlainText("f") QTest.mouseClick(w.bt_select_changed, Qt.LeftButton) selected_rc = dict(w.tree.selected_rc()) self.assertEqual(len(selected_rc), 1, msg=selected_rc) - self.assertIn('console.auto_set_mp', selected_rc) - self.assertEqual(selected_rc['console.auto_set_mp'], False) + self.assertIn("console.auto_set_mp", selected_rc) + self.assertEqual(selected_rc["console.auto_set_mp"], False) for item in items: - if item.text(0) == 'console.auto_set_mp': + if item.text(0) == "console.auto_set_mp": iw = w.tree.itemWidget(item.child(0), 2) - iw.setPlainText('t') + iw.setPlainText("t") QTest.mouseClick(w.bt_select_none, Qt.LeftButton) self.assertFalse(dict(w.tree.selected_rc())) @@ -89,15 +96,15 @@ class TestRcParamsWidget(bt.PsyPlotGuiTestCase): # choose an item for i, item in enumerate(w.tree.top_level_items): - if asstring(item.text(0)) == 'console.auto_set_mp': + if asstring(item.text(0)) == "console.auto_set_mp": break self.assertTrue(w.is_valid, msg=w.tree.valid) # set an invalid value - w.tree.itemWidget(item.child(0), 2).setPlainText('tg') + w.tree.itemWidget(item.child(0), 2).setPlainText("tg") self.assertFalse(w.tree.valid[i]) self.assertFalse(w.is_valid) - w.tree.itemWidget(item.child(0), 2).setPlainText('t') + w.tree.itemWidget(item.child(0), 2).setPlainText("t") self.assertTrue(w.tree.valid[i]) self.assertTrue(w.is_valid) @@ -115,7 +122,8 @@ class TestRcParamsWidget(bt.PsyPlotGuiTestCase): self.assertEqual(len(w.tree.selectedItems()), len(gui_rcParams)) fname = tempfile.NamedTemporaryFile( - prefix='psyplot_gui_test', suffix='.yml').name + prefix="psyplot_gui_test", suffix=".yml" + ).name self._created_files.add(fname) action = w.save_settings_action(target=fname) action.trigger() @@ -139,7 +147,8 @@ class TestRcParamsWidget(bt.PsyPlotGuiTestCase): self.assertEqual(len(w.tree.selectedItems()), len(keys)) fname = tempfile.NamedTemporaryFile( - prefix='psyplot_gui_test', suffix='.yml').name + prefix="psyplot_gui_test", suffix=".yml" + ).name self._created_files.add(fname) action = w.save_settings_action(target=fname) action.trigger() @@ -155,14 +164,15 @@ class TestRcParamsWidget(bt.PsyPlotGuiTestCase): w.initialize() fname = tempfile.NamedTemporaryFile( - prefix='psyplot_gui_test', suffix='.yml').name + prefix="psyplot_gui_test", suffix=".yml" + ).name self._created_files.add(fname) - gui_rcParams.find_all('console').dump(fname) + gui_rcParams.find_all("console").dump(fname) keys = [] for item in w.tree.top_level_items: - if asstring(item.text(0)).startswith('help_explorer'): + if asstring(item.text(0)).startswith("help_explorer"): item.setSelected(True) keys.append(asstring(item.text(0))) @@ -176,8 +186,12 @@ class TestRcParamsWidget(bt.PsyPlotGuiTestCase): self.assertEqual( dict(rc), - {key: gui_rcParams[key] for key in gui_rcParams - if key.startswith('console') or key.startswith('help_explorer')}) + { + key: gui_rcParams[key] + for key in gui_rcParams + if key.startswith("console") or key.startswith("help_explorer") + }, + ) class TestPreferences(bt.PsyPlotGuiTestCase): @@ -201,39 +215,47 @@ class TestPreferences(bt.PsyPlotGuiTestCase): self.assertTrue(pages) self.assertTrue( any(isinstance(p, prefs.GuiRcParamsWidget) for p in pages), - msg=pages) + msg=pages, + ) self.assertTrue( any(isinstance(p, prefs.PsyRcParamsWidget) for p in pages), - msg=pages) + msg=pages, + ) def test_apply(self): """Test the apply button""" pref_w = self.prefs - i, cp = next(t for t in enumerate(pref_w.pages) - if isinstance(t[1], prefs.GuiRcParamsWidget)) + i, cp = next( + t + for t in enumerate(pref_w.pages) + if isinstance(t[1], prefs.GuiRcParamsWidget) + ) pref_w.set_current_index(i) self.assertIsInstance(pref_w.get_page(), prefs.GuiRcParamsWidget) self.assertFalse(pref_w.bt_apply.isEnabled()) # change a value - current = gui_rcParams['console.auto_set_mp'] + current = gui_rcParams["console.auto_set_mp"] for item in cp.tree.top_level_items: - if item.text(0) == 'console.auto_set_mp': + if item.text(0) == "console.auto_set_mp": break cp.tree.itemWidget(item.child(0), 2).setPlainText( - yaml.dump(not current)) + yaml.dump(not current) + ) self.assertTrue(pref_w.bt_apply.isEnabled()) QTest.mouseClick(pref_w.bt_apply, Qt.LeftButton) - self.assertEqual(gui_rcParams['console.auto_set_mp'], not current) + self.assertEqual(gui_rcParams["console.auto_set_mp"], not current) self.assertFalse(pref_w.bt_apply.isEnabled()) # change the value and the page - cp.tree.itemWidget(item.child(0), 2).setPlainText( - yaml.dump(current)) + cp.tree.itemWidget(item.child(0), 2).setPlainText(yaml.dump(current)) self.assertTrue(pref_w.bt_apply.isEnabled()) - j, cp2 = next(t for t in enumerate(pref_w.pages) - if isinstance(t[1], prefs.PsyRcParamsWidget)) + j, cp2 = next( + t + for t in enumerate(pref_w.pages) + if isinstance(t[1], prefs.PsyRcParamsWidget) + ) pref_w.set_current_index(j) self.assertIsInstance(pref_w.get_page(), prefs.PsyRcParamsWidget) self.assertFalse(pref_w.bt_apply.isEnabled()) @@ -243,40 +265,47 @@ class TestPreferences(bt.PsyPlotGuiTestCase): def test_ok(self): """Test the apply button""" pref_w = self.prefs - i, cp = next(t for t in enumerate(pref_w.pages) - if isinstance(t[1], prefs.GuiRcParamsWidget)) + i, cp = next( + t + for t in enumerate(pref_w.pages) + if isinstance(t[1], prefs.GuiRcParamsWidget) + ) pref_w.set_current_index(i) self.assertIsInstance(pref_w.get_page(), prefs.GuiRcParamsWidget) self.assertFalse(pref_w.bt_apply.isEnabled()) # change a value - current = gui_rcParams['console.auto_set_mp'] + current = gui_rcParams["console.auto_set_mp"] for item in cp.tree.top_level_items: - if item.text(0) == 'console.auto_set_mp': + if item.text(0) == "console.auto_set_mp": break cp.tree.itemWidget(item.child(0), 2).setPlainText( - yaml.dump(not current)) + yaml.dump(not current) + ) self.assertTrue(pref_w.bt_apply.isEnabled()) # change a value in the PsyRcParamsWidget - i, cp = next(t for t in enumerate(pref_w.pages) - if isinstance(t[1], prefs.PsyRcParamsWidget)) + i, cp = next( + t + for t in enumerate(pref_w.pages) + if isinstance(t[1], prefs.PsyRcParamsWidget) + ) pref_w.set_current_index(i) self.assertIsInstance(pref_w.get_page(), prefs.PsyRcParamsWidget) self.assertFalse(pref_w.bt_apply.isEnabled()) # change a value for item in cp.tree.top_level_items: - if item.text(0) == 'decoder.x': + if item.text(0) == "decoder.x": break - cp.tree.itemWidget(item.child(0), 2).setPlainText( - yaml.dump({'test'})) + cp.tree.itemWidget(item.child(0), 2).setPlainText(yaml.dump({"test"})) self.assertTrue(pref_w.bt_apply.isEnabled()) - QTest.mouseClick(pref_w.bbox.button(QDialogButtonBox.Ok), - Qt.LeftButton) - self.assertEqual(gui_rcParams['console.auto_set_mp'], not current) - self.assertEqual(psy_rcParams['decoder.x'], {'test'}) + QTest.mouseClick( + pref_w.bbox.button(QDialogButtonBox.Ok), Qt.LeftButton + ) + self.assertEqual(gui_rcParams["console.auto_set_mp"], not current) + self.assertEqual(psy_rcParams["decoder.x"], {"test"}) def test_plugin_pages(self): try: @@ -285,15 +314,19 @@ class TestPreferences(bt.PsyPlotGuiTestCase): self.skipTest("psyplot_gui_test not installed") pref_w = self.prefs QTest.mouseClick(pref_w.bt_load_plugins, Qt.LeftButton) - i, cp = next(t for t in enumerate(pref_w.pages) - if isinstance(t[1], prefs.RcParamsWidget) and - not isinstance(t[1], prefs.PsyRcParamsWidget) and - 'test_plugin' in ( - item.text(0) for item in t[1].tree.top_level_items)) + i, cp = next( + t + for t in enumerate(pref_w.pages) + if isinstance(t[1], prefs.RcParamsWidget) + and not isinstance(t[1], prefs.PsyRcParamsWidget) + and "test_plugin" + in (item.text(0) for item in t[1].tree.top_level_items) + ) pref_w.set_current_index(i) - self.assertEqual(len(list(cp.tree.top_level_items)), len(rcParams), - msg=cp) + self.assertEqual( + len(list(cp.tree.top_level_items)), len(rcParams), msg=cp + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_project_content.py b/tests/test_project_content.py index df69c30f2d26d575c046240a877116c8690d0b63..f156eec749e20bf47af76d56f870b610fb98933a 100644 --- a/tests/test_project_content.py +++ b/tests/test_project_content.py @@ -1,12 +1,19 @@ -# -*- coding: utf-8 -*- -from collections import defaultdict +# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +# +# SPDX-License-Identifier: LGPL-3.0-only + import os.path as osp import unittest + +# -*- coding: utf-8 -*- +from collections import defaultdict + import _base_testing as bt -import xarray as xr import psyplot.data as psyd import psyplot.project as psy -from psyplot_gui.compat.qtcompat import QTest, Qt, QDialogButtonBox, asstring +import xarray as xr + +from psyplot_gui.compat.qtcompat import QDialogButtonBox, Qt, QTest, asstring class ProjectContentTest(bt.PsyPlotGuiTestCase): @@ -25,8 +32,9 @@ class ProjectContentTest(bt.PsyPlotGuiTestCase): return self.content_widget.lists[key] def _selected_rows(self, name): - return list(map(lambda ind: ind.row(), - self.get_list(name).selectedIndexes())) + return list( + map(lambda ind: ind.row(), self.get_list(name).selectedIndexes()) + ) def test_content_update(self): """Test whether the list is updated correctly""" @@ -34,69 +42,91 @@ class ProjectContentTest(bt.PsyPlotGuiTestCase): lists = w.lists # currently it should be empty self.assertEqual(w.count(), 1) - self.assertEqual(w.indexOf(lists['All']), 0) + self.assertEqual(w.indexOf(lists["All"]), 0) self.assertFalse(w.isItemEnabled(0), msg='List "All" is enabled!') # create some plots - sp = psy.plot.plot2d(self.get_file('test-t2m-u-v.nc'), name='t2m') - sp2 = psy.plot.lineplot(self.get_file('test-t2m-u-v.nc'), name='t2m', - t=0, x=0, y=0) + sp = psy.plot.plot2d(self.get_file("test-t2m-u-v.nc"), name="t2m") + sp2 = psy.plot.lineplot( + self.get_file("test-t2m-u-v.nc"), name="t2m", t=0, x=0, y=0 + ) d = defaultdict(lambda: 1) - d['All'] = 2 - d['simple'] = 2 - for name in ['All', 'simple', 'lineplot', 'plot2d']: + d["All"] = 2 + d["simple"] = 2 + for name in ["All", "simple", "lineplot", "plot2d"]: self.assertIn(name, lists) - l = lists[name] - i = self.content_widget.indexOf(l) - self.assertNotEqual(i, -1, msg='Missing the list in the widget!') - self.assertTrue(self.content_widget.isItemEnabled(i), - msg='%s is not enabled!' % name) - self.assertEqual(l.count(), d[name], - msg='Wrong number of arrays in %s' % name) - if name == 'plot2d': - self.assertEqual(asstring(l.item(0).text()), - sp[0].psy._short_info(), - msg='Wrong text in plot2d') + arrays = lists[name] + i = self.content_widget.indexOf(arrays) + self.assertNotEqual(i, -1, msg="Missing the list in the widget!") + self.assertTrue( + self.content_widget.isItemEnabled(i), + msg="%s is not enabled!" % name, + ) + self.assertEqual( + arrays.count(), + d[name], + msg="Wrong number of arrays in %s" % name, + ) + if name == "plot2d": + self.assertEqual( + asstring(arrays.item(0).text()), + sp[0].psy._short_info(), + msg="Wrong text in plot2d", + ) else: self.assertEqual( - asstring(l.item(d[name] - 1).text()), sp2[0]._short_info(), - msg='Wrong text in %s' % name) - self.assertEqual(self._selected_rows('plot2d'), [], - msg='Array in %s is wrongly selected!' % name) - self.assertEqual(self._selected_rows('lineplot'), [0], - msg='Array in %s is not selected!' % name) - self.assertEqual(self._selected_rows('simple'), [1], - msg='Wrong selection!') - self.assertEqual(self._selected_rows('All'), [1], - msg='Wrong selection!') + asstring(arrays.item(d[name] - 1).text()), + sp2[0]._short_info(), + msg="Wrong text in %s" % name, + ) + self.assertEqual( + self._selected_rows("plot2d"), + [], + msg="Array in %s is wrongly selected!" % name, + ) + self.assertEqual( + self._selected_rows("lineplot"), + [0], + msg="Array in %s is not selected!" % name, + ) + self.assertEqual( + self._selected_rows("simple"), [1], msg="Wrong selection!" + ) + self.assertEqual( + self._selected_rows("All"), [1], msg="Wrong selection!" + ) # close the project full = sp + sp2 full.close(True, True, True) self.assertEqual(w.count(), 1) - self.assertEqual(w.indexOf(lists['All']), 0) + self.assertEqual(w.indexOf(lists["All"]), 0) self.assertFalse(w.isItemEnabled(0), msg='List "All" is enabled!') def test_select_all_button(self): - """Test whether the subproject is changed correctly when selecting all - """ + """Test whether the subproject is changed correctly when selecting all""" self.window.showMaximized() - sp = psy.plot.plot2d(self.get_file('test-t2m-u-v.nc'), name='t2m', - time=[0, 1]) + sp = psy.plot.plot2d( + self.get_file("test-t2m-u-v.nc"), name="t2m", time=[0, 1] + ) psy.scp(None) QTest.mouseClick(self.project_content.select_all_button, Qt.LeftButton) - self.assertIs(psy.gcp()[0], sp[0], - msg='actual: %s, expected: %s' % (psy.gcp(), sp)) + self.assertIs( + psy.gcp()[0], + sp[0], + msg="actual: %s, expected: %s" % (psy.gcp(), sp), + ) self.assertIs(psy.gcp()[1], sp[1]) - self.assertEqual(self._selected_rows('All'), [0, 1], - msg='Not all arrays selected!') + self.assertEqual( + self._selected_rows("All"), [0, 1], msg="Not all arrays selected!" + ) def test_unselect_all_button(self): - """Test whether the subproject is changed cleared when unselecting all - """ + """Test whether the subproject is changed cleared when unselecting all""" self.window.showMaximized() - psy.plot.plot2d(self.get_file('test-t2m-u-v.nc'), name='t2m', - time=[0, 1]) + psy.plot.plot2d( + self.get_file("test-t2m-u-v.nc"), name="t2m", time=[0, 1] + ) # test whether the current subproject is not empty self.assertTrue(bool(psy.gcp())) # unselect all @@ -106,15 +136,16 @@ class ProjectContentTest(bt.PsyPlotGuiTestCase): def test_item_selection(self): """Test whether the subproject is changed correctly if the selection in the list changes""" - sp = psy.plot.plot2d(self.get_file('test-t2m-u-v.nc'), name='t2m', - time=[0, 1]) + sp = psy.plot.plot2d( + self.get_file("test-t2m-u-v.nc"), name="t2m", time=[0, 1] + ) self.assertIs(psy.gcp()[0], sp[0]) self.assertIs(psy.gcp()[1], sp[1]) - self.content_widget.lists['All'].item(0).setSelected(False) + self.content_widget.lists["All"].item(0).setSelected(False) self.assertIs(psy.gcp()[0], sp[1]) - self.content_widget.lists['All'].item(0).setSelected(True) - self.assertIs(psy.gcp()[0], sp[0], msg='Reselection failed!') - self.assertIs(psy.gcp()[1], sp[1], msg='Reselection failed!') + self.content_widget.lists["All"].item(0).setSelected(True) + self.assertIs(psy.gcp()[0], sp[0], msg="Reselection failed!") + self.assertIs(psy.gcp()[1], sp[1], msg="Reselection failed!") class FiguresTreeTest(bt.PsyPlotGuiTestCase): @@ -126,35 +157,53 @@ class FiguresTreeTest(bt.PsyPlotGuiTestCase): def test_toplevel(self): """Test whether the figures are updated correctly""" + def check_figs(msg=None): figs = iter(sp.figs) - for item in map(self.tree.topLevelItem, - range(self.tree.topLevelItemCount())): - self.assertEqual(asstring(item.text(0)), - next(figs).canvas.get_window_title(), msg=msg) - sp = psy.plot.plot2d(self.get_file('test-t2m-u-v.nc'), name='t2m', - time=[0, 1]) + for item in map( + self.tree.topLevelItem, range(self.tree.topLevelItemCount()) + ): + self.assertEqual( + asstring(item.text(0)), + next(figs).canvas.manager.get_window_title(), + msg=msg, + ) + + sp = psy.plot.plot2d( + self.get_file("test-t2m-u-v.nc"), name="t2m", time=[0, 1] + ) check_figs() sp[1:].close(True, True) - check_figs('Figures not updated correctly!') + check_figs("Figures not updated correctly!") def test_sublevel(self): """Test whether the arrays are updated correctly""" + def check_figs(msg=None): arrays = iter(sp) for i, (fig, val) in enumerate(sp.figs.items()): top = self.tree.topLevelItem(i) - self.assertEqual(asstring(top.text(0)), - fig.canvas.get_window_title()) + self.assertEqual( + asstring(top.text(0)), + fig.canvas.manager.get_window_title(), + ) for child in map(top.child, range(top.childCount())): - self.assertEqual(asstring(child.text(0)), - next(arrays).psy._short_info(), msg=msg) - sp = psy.plot.plot2d(self.get_file('test-t2m-u-v.nc'), name='t2m', - time=[0, 1, 2], ax=(1, 2)) + self.assertEqual( + asstring(child.text(0)), + next(arrays).psy._short_info(), + msg=msg, + ) + + sp = psy.plot.plot2d( + self.get_file("test-t2m-u-v.nc"), + name="t2m", + time=[0, 1, 2], + ax=(1, 2), + ) check_figs() sp[1:2].close(False, True) sp = sp[0::2] - check_figs('Arrays not updated correctly!') + check_figs("Arrays not updated correctly!") sp.close(True, True) self.assertEqual(self.tree.topLevelItemCount(), 0) @@ -168,69 +217,84 @@ class DatasetTreeTest(bt.PsyPlotGuiTestCase): def test_toplevel(self): """Test whether the toplevel items are shown correctly""" - fname = self.get_file('test-t2m-u-v.nc') - sp1 = psy.plot.plot2d(fname, name='t2m') - sp2 = psy.plot.plot2d(fname, name='t2m') + fname = self.get_file("test-t2m-u-v.nc") + sp1 = psy.plot.plot2d(fname, name="t2m") + sp2 = psy.plot.plot2d(fname, name="t2m") count = next(psyd._ds_counter) - 1 fname = osp.basename(fname) ds1 = sp1[0].psy.base ds2 = sp2[0].psy.base self.assertEqual(self.tree.topLevelItemCount(), 2) - self.assertEqual(asstring(self._get_toplevel_item(ds1).text(0)), - '%i: %s' % (count - 1, fname)) - self.assertEqual(asstring(self._get_toplevel_item(ds2).text(0)), - '*%i: %s' % (count, fname)) + self.assertEqual( + asstring(self._get_toplevel_item(ds1).text(0)), + "%i: %s" % (count - 1, fname), + ) + self.assertEqual( + asstring(self._get_toplevel_item(ds2).text(0)), + "*%i: %s" % (count, fname), + ) psy.scp(sp1) - self.assertEqual(asstring(self._get_toplevel_item(ds1).text(0)), - '*%i: %s' % (count - 1, fname)) - self.assertEqual(asstring(self._get_toplevel_item(ds2).text(0)), - '%i: %s' % (count, fname)) + self.assertEqual( + asstring(self._get_toplevel_item(ds1).text(0)), + "*%i: %s" % (count - 1, fname), + ) + self.assertEqual( + asstring(self._get_toplevel_item(ds2).text(0)), + "%i: %s" % (count, fname), + ) sp2.close(True, True) - self.assertEqual(asstring(self._get_toplevel_item(ds1).text(0)), - '*%i: %s' % (count - 1, fname)) + self.assertEqual( + asstring(self._get_toplevel_item(ds1).text(0)), + "*%i: %s" % (count - 1, fname), + ) self.assertEqual(self.tree.topLevelItemCount(), 1) def _get_toplevel_item(self, ds): toplevel = None - for item in map(self.tree.topLevelItem, - range(self.tree.topLevelItemCount())): + for item in map( + self.tree.topLevelItem, range(self.tree.topLevelItemCount()) + ): if item.ds() is ds: toplevel = item break self.assertIsNotNone( - toplevel, msg='No item found that corresponds to %s' % ds) + toplevel, msg="No item found that corresponds to %s" % ds + ) return toplevel def _test_ds_representation(self, ds): toplevel = self._get_toplevel_item(ds) coords = set(ds.coords) variables = set(ds.variables) - coords - for child in map(toplevel.variables.child, - range(toplevel.variables.childCount())): + for child in map( + toplevel.variables.child, range(toplevel.variables.childCount()) + ): variables.remove(asstring(child.text(0))) - self.assertEqual(len(variables), 0, msg='Variables not found: %s' % ( - variables)) - for child in map(toplevel.coords.child, - range(toplevel.coords.childCount())): + self.assertEqual( + len(variables), 0, msg="Variables not found: %s" % (variables) + ) + for child in map( + toplevel.coords.child, range(toplevel.coords.childCount()) + ): coords.remove(asstring(child.text(0))) - self.assertEqual(len(coords), 0, msg='Coordinates not found: %s' % ( - coords)) + self.assertEqual( + len(coords), 0, msg="Coordinates not found: %s" % (coords) + ) def test_sublevel(self): - """Test whether the variables and coordinates are displayed correctly - """ - sp = psy.plot.plot2d(self.get_file('test-t2m-u-v.nc'), name='t2m') + """Test whether the variables and coordinates are displayed correctly""" + sp = psy.plot.plot2d(self.get_file("test-t2m-u-v.nc"), name="t2m") ds = sp[0].psy.base self._test_ds_representation(ds) def test_refresh(self): """Test the refreshing of a dataset""" - fname = self.get_file('test-t2m-u-v.nc') - sp1 = psy.plot.plot2d(fname, name='t2m') - sp2 = psy.plot.plot2d(fname, name='t2m') + fname = self.get_file("test-t2m-u-v.nc") + sp1 = psy.plot.plot2d(fname, name="t2m") + sp2 = psy.plot.plot2d(fname, name="t2m") ds = sp1[0].psy.base - ds['test'] = xr.Variable(('testdim', ), list(range(5))) + ds["test"] = xr.Variable(("testdim",), list(range(5))) item = self.tree.topLevelItem(0) self.tree.refresh_items(item) self._test_ds_representation(ds) @@ -238,26 +302,26 @@ class DatasetTreeTest(bt.PsyPlotGuiTestCase): def test_refresh_all(self): """Test the refreshing of a dataset""" - fname = self.get_file('test-t2m-u-v.nc') - sp1 = psy.plot.plot2d(fname, name='t2m') - sp2 = psy.plot.plot2d(fname, name='t2m') + fname = self.get_file("test-t2m-u-v.nc") + sp1 = psy.plot.plot2d(fname, name="t2m") + sp2 = psy.plot.plot2d(fname, name="t2m") ds = sp1[0].psy.base - ds['test'] = xr.Variable(('testdim', ), list(range(5))) + ds["test"] = xr.Variable(("testdim",), list(range(5))) ds2 = sp2[0].psy.base - ds2['test2'] = list(range(10)) + ds2["test2"] = list(range(10)) self.tree.refresh_items() self._test_ds_representation(ds) self._test_ds_representation(ds2) def test_expansion_reset(self): """Test whether the expansion state is recovered""" - fname = self.get_file('test-t2m-u-v.nc') - psy.plot.plot2d(fname, name='t2m') + fname = self.get_file("test-t2m-u-v.nc") + psy.plot.plot2d(fname, name="t2m") self.tree.expandItem(self.tree.topLevelItem(0)) self.tree.expandItem(self.tree.topLevelItem(0).child(1)) # trigger an update - psy.plot.plot2d(fname, name='t2m') + psy.plot.plot2d(fname, name="t2m") self.assertTrue(self.tree.topLevelItem(0).isExpanded()) self.assertFalse(self.tree.topLevelItem(0).child(0).isExpanded()) self.assertTrue(self.tree.topLevelItem(0).child(1).isExpanded()) @@ -265,23 +329,24 @@ class DatasetTreeTest(bt.PsyPlotGuiTestCase): def test_make_plot(self): """Test the making of plots""" - fname = self.get_file('test-t2m-u-v.nc') - sp1 = psy.plot.plot2d(fname, name='t2m') + fname = self.get_file("test-t2m-u-v.nc") + sp1 = psy.plot.plot2d(fname, name="t2m") # to make sure, have in the mean time another dataset in the current # subproject, we create a second plot - psy.plot.plot2d(fname, name='t2m') + psy.plot.plot2d(fname, name="t2m") ds = sp1[0].psy.base - name = 't2m' + name = "t2m" self.tree.make_plot(ds, name) try: - self.window.plot_creator.pm_combo.setCurrentText('plot2d') + self.window.plot_creator.pm_combo.setCurrentText("plot2d") except AttributeError: - self.window.plot_creator.pm_combo.setEditText('plot2d') + self.window.plot_creator.pm_combo.setEditText("plot2d") QTest.mouseClick( self.window.plot_creator.bbox.button(QDialogButtonBox.Ok), - Qt.LeftButton) + Qt.LeftButton, + ) self.assertIs(ds, psy.gcp()[0].psy.base) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000000000000000000000000000000000000..39e7d4f11d9f6d4270b57f29ca5ee82e82a8b91d --- /dev/null +++ b/tox.ini @@ -0,0 +1,24 @@ +; SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH +; +; SPDX-License-Identifier: CC0-1.0 + +[tox] + +[testenv] +extras = + testsite + +commands = + ; mypy psyplot_gui + isort --check psyplot_gui + black --line-length 79 --check psyplot_gui + ; blackdoc --check psyplot_gui + flake8 psyplot_gui + pytest -v --cov=psyplot_gui -x + reuse lint + cffconvert --validate + +[pytest] +DJANGO_SETTINGS_MODULE = testproject.settings +python_files = tests.py test_*.py *_tests.py +norecursedirs = .* build dist *.egg venv docs diff --git a/versioneer.py b/versioneer.py deleted file mode 100644 index 745b458fbe8ee1097f67a8531d90c84b75e3c77e..0000000000000000000000000000000000000000 --- a/versioneer.py +++ /dev/null @@ -1,1849 +0,0 @@ - -# Version: 0.18 - -"""The Versioneer - like a rocketeer, but for versions. - -The Versioneer -============== - -* like a rocketeer, but for versions! -* https://github.com/warner/python-versioneer -* Brian Warner -* License: Public Domain -* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy -* [![Latest Version] -(https://pypip.in/version/versioneer/badge.svg?style=flat) -](https://pypi.python.org/pypi/versioneer/) -* [![Build Status] -(https://travis-ci.org/warner/python-versioneer.png?branch=master) -](https://travis-ci.org/warner/python-versioneer) - -This is a tool for managing a recorded version number in distutils-based -python projects. The goal is to remove the tedious and error-prone "update -the embedded version string" step from your release process. Making a new -release should be as easy as recording a new tag in your version-control -system, and maybe making new tarballs. - - -## Quick Install - -* `pip install versioneer` to somewhere to your $PATH -* add a `[versioneer]` section to your setup.cfg (see below) -* run `versioneer install` in your source tree, commit the results - -## Version Identifiers - -Source trees come from a variety of places: - -* a version-control system checkout (mostly used by developers) -* a nightly tarball, produced by build automation -* a snapshot tarball, produced by a web-based VCS browser, like github's - "tarball from tag" feature -* a release tarball, produced by "setup.py sdist", distributed through PyPI - -Within each source tree, the version identifier (either a string or a number, -this tool is format-agnostic) can come from a variety of places: - -* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows - about recent "tags" and an absolute revision-id -* the name of the directory into which the tarball was unpacked -* an expanded VCS keyword ($Id$, etc) -* a `_version.py` created by some earlier build step - -For released software, the version identifier is closely related to a VCS -tag. Some projects use tag names that include more than just the version -string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool -needs to strip the tag prefix to extract the version identifier. For -unreleased software (between tags), the version identifier should provide -enough information to help developers recreate the same tree, while also -giving them an idea of roughly how old the tree is (after version 1.2, before -version 1.3). Many VCS systems can report a description that captures this, -for example `git describe --tags --dirty --always` reports things like -"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the -0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has -uncommitted changes. - -The version identifier is used for multiple purposes: - -* to allow the module to self-identify its version: `myproject.__version__` -* to choose a name and prefix for a 'setup.py sdist' tarball - -## Theory of Operation - -Versioneer works by adding a special `_version.py` file into your source -tree, where your `__init__.py` can import it. This `_version.py` knows how to -dynamically ask the VCS tool for version information at import time. - -`_version.py` also contains `$Revision$` markers, and the installation -process marks `_version.py` to have this marker rewritten with a tag name -during the `git archive` command. As a result, generated tarballs will -contain enough information to get the proper version. - -To allow `setup.py` to compute a version too, a `versioneer.py` is added to -the top level of your source tree, next to `setup.py` and the `setup.cfg` -that configures it. This overrides several distutils/setuptools commands to -compute the version when invoked, and changes `setup.py build` and `setup.py -sdist` to replace `_version.py` with a small static file that contains just -the generated version data. - -## Installation - -See [INSTALL.md](./INSTALL.md) for detailed installation instructions. - -## Version-String Flavors - -Code which uses Versioneer can learn about its version string at runtime by -importing `_version` from your main `__init__.py` file and running the -`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can -import the top-level `versioneer.py` and run `get_versions()`. - -Both functions return a dictionary with different flavors of version -information: - -* `['version']`: A condensed version string, rendered using the selected - style. This is the most commonly used value for the project's version - string. The default "pep440" style yields strings like `0.11`, - `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section - below for alternative styles. - -* `['full-revisionid']`: detailed revision identifier. For Git, this is the - full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". - -* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the - commit date in ISO 8601 format. This will be None if the date is not - available. - -* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that - this is only accurate if run in a VCS checkout, otherwise it is likely to - be False or None - -* `['error']`: if the version string could not be computed, this will be set - to a string describing the problem, otherwise it will be None. It may be - useful to throw an exception in setup.py if this is set, to avoid e.g. - creating tarballs with a version string of "unknown". - -Some variants are more useful than others. Including `full-revisionid` in a -bug report should allow developers to reconstruct the exact code being tested -(or indicate the presence of local changes that should be shared with the -developers). `version` is suitable for display in an "about" box or a CLI -`--version` output: it can be easily compared against release notes and lists -of bugs fixed in various releases. - -The installer adds the following text to your `__init__.py` to place a basic -version in `YOURPROJECT.__version__`: - - from ._version import get_versions - __version__ = get_versions()['version'] - del get_versions - -## Styles - -The setup.cfg `style=` configuration controls how the VCS information is -rendered into a version string. - -The default style, "pep440", produces a PEP440-compliant string, equal to the -un-prefixed tag name for actual releases, and containing an additional "local -version" section with more detail for in-between builds. For Git, this is -TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags ---dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the -tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and -that this commit is two revisions ("+2") beyond the "0.11" tag. For released -software (exactly equal to a known tag), the identifier will only contain the -stripped tag, e.g. "0.11". - -Other styles are available. See [details.md](details.md) in the Versioneer -source tree for descriptions. - -## Debugging - -Versioneer tries to avoid fatal errors: if something goes wrong, it will tend -to return a version of "0+unknown". To investigate the problem, run `setup.py -version`, which will run the version-lookup code in a verbose mode, and will -display the full contents of `get_versions()` (including the `error` string, -which may help identify what went wrong). - -## Known Limitations - -Some situations are known to cause problems for Versioneer. This details the -most significant ones. More can be found on Github -[issues page](https://github.com/warner/python-versioneer/issues). - -### Subprojects - -Versioneer has limited support for source trees in which `setup.py` is not in -the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are -two common reasons why `setup.py` might not be in the root: - -* Source trees which contain multiple subprojects, such as - [Buildbot](https://github.com/buildbot/buildbot), which contains both - "master" and "slave" subprojects, each with their own `setup.py`, - `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI - distributions (and upload multiple independently-installable tarballs). -* Source trees whose main purpose is to contain a C library, but which also - provide bindings to Python (and perhaps other langauges) in subdirectories. - -Versioneer will look for `.git` in parent directories, and most operations -should get the right version string. However `pip` and `setuptools` have bugs -and implementation details which frequently cause `pip install .` from a -subproject directory to fail to find a correct version string (so it usually -defaults to `0+unknown`). - -`pip install --editable .` should work correctly. `setup.py install` might -work too. - -Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in -some later version. - -[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking -this issue. The discussion in -[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the -issue from the Versioneer side in more detail. -[pip PR#3176](https://github.com/pypa/pip/pull/3176) and -[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve -pip to let Versioneer work correctly. - -Versioneer-0.16 and earlier only looked for a `.git` directory next to the -`setup.cfg`, so subprojects were completely unsupported with those releases. - -### Editable installs with setuptools <= 18.5 - -`setup.py develop` and `pip install --editable .` allow you to install a -project into a virtualenv once, then continue editing the source code (and -test) without re-installing after every change. - -"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a -convenient way to specify executable scripts that should be installed along -with the python package. - -These both work as expected when using modern setuptools. When using -setuptools-18.5 or earlier, however, certain operations will cause -`pkg_resources.DistributionNotFound` errors when running the entrypoint -script, which must be resolved by re-installing the package. This happens -when the install happens with one version, then the egg_info data is -regenerated while a different version is checked out. Many setup.py commands -cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into -a different virtualenv), so this can be surprising. - -[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes -this one, but upgrading to a newer version of setuptools should probably -resolve it. - -### Unicode version strings - -While Versioneer works (and is continually tested) with both Python 2 and -Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. -Newer releases probably generate unicode version strings on py2. It's not -clear that this is wrong, but it may be surprising for applications when then -write these strings to a network connection or include them in bytes-oriented -APIs like cryptographic checksums. - -[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates -this question. - - -## Updating Versioneer - -To upgrade your project to a new release of Versioneer, do the following: - -* install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg`, if necessary, to include any new configuration settings - indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. -* re-run `versioneer install` in your source tree, to replace - `SRC/_version.py` -* commit any changed files - -## Future Directions - -This tool is designed to make it easily extended to other version-control -systems: all VCS-specific components are in separate directories like -src/git/ . The top-level `versioneer.py` script is assembled from these -components by running make-versioneer.py . In the future, make-versioneer.py -will take a VCS name as an argument, and will construct a version of -`versioneer.py` that is specific to the given VCS. It might also take the -configuration arguments that are currently provided manually during -installation by editing setup.py . Alternatively, it might go the other -direction and include code from all supported VCS systems, reducing the -number of intermediate scripts. - - -## License - -To make Versioneer easier to embed, all its code is dedicated to the public -domain. The `_version.py` that it creates is also in the public domain. -Specifically, both are released under the Creative Commons "Public Domain -Dedication" license (CC0-1.0), as described in -https://creativecommons.org/publicdomain/zero/1.0/ . - -""" - -# Disclaimer -# ---------- -# -# Copyright (C) 2021 Helmholtz-Zentrum Hereon -# Copyright (C) 2020-2021 Helmholtz-Zentrum Geesthacht -# Copyright (C) 2016-2021 University of Lausanne -# -# This file is part of psyplot-gui and is released under the GNU LGPL-3.O license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License version 3.0 as -# published by the Free Software Foundation. -# -# 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 LGPL-3.0 license for more details. -# -# You should have received a copy of the GNU LGPL-3.0 license -# along with this program. If not, see <https://www.gnu.org/licenses/>. -# -# This file is originally released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) - - -from __future__ import print_function -try: - import configparser -except ImportError: - import ConfigParser as configparser -import errno -import json -import os -import re -import subprocess -import sys - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_root(): - """Get the project root directory. - - We require that all commands are run from the project root, i.e. the - directory that contains setup.py, setup.cfg, and versioneer.py . - """ - root = os.path.realpath(os.path.abspath(os.getcwd())) - setup_py = os.path.join(root, "setup.py") - versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - # allow 'python path/to/setup.py COMMAND' - root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) - setup_py = os.path.join(root, "setup.py") - versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - err = ("Versioneer was unable to run the project root directory. " - "Versioneer requires setup.py to be executed from " - "its immediate directory (like 'python setup.py COMMAND'), " - "or in a way that lets it use sys.argv[0] to find the root " - "(like 'python path/to/setup.py COMMAND').") - raise VersioneerBadRootError(err) - try: - # Certain runtime workflows (setup.py install/develop in a setuptools - # tree) execute all dependencies in a single python process, so - # "versioneer" may be imported multiple times, and python's shared - # module-import table will cache the first one. So we can't use - # os.path.dirname(__file__), as that will find whichever - # versioneer.py was first imported, even in later projects. - me = os.path.realpath(os.path.abspath(__file__)) - me_dir = os.path.normcase(os.path.splitext(me)[0]) - vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) - if me_dir != vsr_dir: - print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(me), versioneer_py)) - except NameError: - pass - return root - - -def get_config_from_root(root): - """Read the project setup.cfg file to determine Versioneer config.""" - # This might raise EnvironmentError (if setup.cfg is missing), or - # configparser.NoSectionError (if it lacks a [versioneer] section), or - # configparser.NoOptionError (if it lacks "VCS="). See the docstring at - # the top of versioneer.py for instructions on writing your setup.cfg . - setup_cfg = os.path.join(root, "setup.cfg") - parser = configparser.SafeConfigParser() - with open(setup_cfg, "r") as f: - parser.readfp(f) - VCS = parser.get("versioneer", "VCS") # mandatory - - def get(parser, name): - if parser.has_option("versioneer", name): - return parser.get("versioneer", name) - return None - cfg = VersioneerConfig() - cfg.VCS = VCS - cfg.style = get(parser, "style") or "" - cfg.versionfile_source = get(parser, "versionfile_source") - cfg.versionfile_build = get(parser, "versionfile_build") - cfg.tag_prefix = get(parser, "tag_prefix") - if cfg.tag_prefix in ("''", '""'): - cfg.tag_prefix = "" - cfg.parentdir_prefix = get(parser, "parentdir_prefix") - cfg.verbose = get(parser, "verbose") - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -# these dictionaries contain VCS-specific tools -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode - - -LONG_VERSION_PY['git'] = ''' -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys - - -def get_keywords(): - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" - git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" - git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_config(): - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "%(STYLE)s" - cfg.tag_prefix = "%(TAG_PREFIX)s" - cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" - cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %%s" %% dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %%s" %% (commands,)) - return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %%s (error)" %% dispcmd) - print("stdout was %%s" %% stdout) - return None, p.returncode - return stdout, p.returncode - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for i in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %%s but none started with prefix %%s" %% - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") - date = keywords.get("date") - if date is not None: - # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %%d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%%s', no digits" %% ",".join(refs - tags)) - if verbose: - print("likely tags: %%s" %% ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %%s" %% r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) - if rc != 0: - if verbose: - print("Directory %%s not under git control" %% root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%%s*" %% tag_prefix], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%%s'" - %% describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%%s' doesn't start with prefix '%%s'" - print(fmt %% (full_tag, tag_prefix)) - pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" - %% (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], - cwd=root)[0].strip() - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post.devDISTANCE - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%%d" %% pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%%d" %% pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%%s" %% pieces["short"] - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%%s" %% pieces["short"] - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Eexceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%%s'" %% style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -def get_versions(): - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} -''' - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") - date = keywords.get("date") - if date is not None: - # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) - if rc != 0: - if verbose: - print("Directory %s not under git control" % root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def do_vcs_install(manifest_in, versionfile_source, ipy): - """Git-specific installation logic for Versioneer. - - For Git, this means creating/changing .gitattributes to mark _version.py - for export-subst keyword substitution. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - files = [manifest_in, versionfile_source] - if ipy: - files.append(ipy) - try: - me = __file__ - if me.endswith(".pyc") or me.endswith(".pyo"): - me = os.path.splitext(me)[0] + ".py" - versioneer_file = os.path.relpath(me) - except NameError: - versioneer_file = "versioneer.py" - files.append(versioneer_file) - present = False - try: - f = open(".gitattributes", "r") - for line in f.readlines(): - if line.strip().startswith(versionfile_source): - if "export-subst" in line.strip().split()[1:]: - present = True - f.close() - except EnvironmentError: - pass - if not present: - f = open(".gitattributes", "a+") - f.write("%s export-subst\n" % versionfile_source) - f.close() - files.append(".gitattributes") - run_command(GITS, ["add", "--"] + files) - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for i in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.18) from -# revision-control system data, or from the parent directory name of an -# unpacked source archive. Distribution tarballs contain a pre-generated copy -# of this file. - -import json - -version_json = ''' -%s -''' # END VERSION_JSON - - -def get_versions(): - return json.loads(version_json) -""" - - -def versions_from_file(filename): - """Try to determine the version from _version.py if present.""" - try: - with open(filename) as f: - contents = f.read() - except EnvironmentError: - raise NotThisMethod("unable to read _version.py") - mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) - if not mo: - mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) - if not mo: - raise NotThisMethod("no version_json in _version.py") - return json.loads(mo.group(1)) - - -def write_to_version_file(filename, versions): - """Write the given version number to the given _version.py file.""" - os.unlink(filename) - contents = json.dumps(versions, sort_keys=True, - indent=1, separators=(",", ": ")) - with open(filename, "w") as f: - f.write(SHORT_VERSION_PY % contents) - - print("set %s to '%s'" % (filename, versions["version"])) - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post.devDISTANCE - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Eexceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -class VersioneerBadRootError(Exception): - """The project root directory is unknown or missing key files.""" - - -def get_versions(verbose=False): - """Get the project version from whatever source is available. - - Returns dict with two keys: 'version' and 'full'. - """ - if "versioneer" in sys.modules: - # see the discussion in cmdclass.py:get_cmdclass() - del sys.modules["versioneer"] - - root = get_root() - cfg = get_config_from_root(root) - - assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" - handlers = HANDLERS.get(cfg.VCS) - assert handlers, "unrecognized VCS '%s'" % cfg.VCS - verbose = verbose or cfg.verbose - assert cfg.versionfile_source is not None, \ - "please set versioneer.versionfile_source" - assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" - - versionfile_abs = os.path.join(root, cfg.versionfile_source) - - # extract version from first of: _version.py, VCS command (e.g. 'git - # describe'), parentdir. This is meant to work for developers using a - # source checkout, for users of a tarball created by 'setup.py sdist', - # and for users of a tarball/zipball created by 'git archive' or github's - # download-from-tag feature or the equivalent in other VCSes. - - get_keywords_f = handlers.get("get_keywords") - from_keywords_f = handlers.get("keywords") - if get_keywords_f and from_keywords_f: - try: - keywords = get_keywords_f(versionfile_abs) - ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) - if verbose: - print("got version from expanded keyword %s" % ver) - return ver - except NotThisMethod: - pass - - try: - ver = versions_from_file(versionfile_abs) - if verbose: - print("got version from file %s %s" % (versionfile_abs, ver)) - return ver - except NotThisMethod: - pass - - from_vcs_f = handlers.get("pieces_from_vcs") - if from_vcs_f: - try: - pieces = from_vcs_f(cfg.tag_prefix, root, verbose) - ver = render(pieces, cfg.style) - if verbose: - print("got version from VCS %s" % ver) - return ver - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - if verbose: - print("got version from parentdir %s" % ver) - return ver - except NotThisMethod: - pass - - if verbose: - print("unable to compute version") - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, "error": "unable to compute version", - "date": None} - - -def get_version(): - """Get the short version string for this project.""" - return get_versions()["version"] - - -def get_cmdclass(cmdclass=None): - """Get the custom setuptools/distutils subclasses used by Versioneer.""" - if "versioneer" in sys.modules: - del sys.modules["versioneer"] - # this fixes the "python setup.py develop" case (also 'install' and - # 'easy_install .'), in which subdependencies of the main project are - # built (using setup.py bdist_egg) in the same python process. Assume - # a main project A and a dependency B, which use different versions - # of Versioneer. A's setup.py imports A's Versioneer, leaving it in - # sys.modules by the time B's setup.py is executed, causing B to run - # with the wrong versioneer. Setuptools wraps the sub-dep builds in a - # sandbox that restores sys.modules to it's pre-build state, so the - # parent is protected against the child's "import versioneer". By - # removing ourselves from sys.modules here, before the child build - # happens, we protect the child from the parent's versioneer too. - # Also see https://github.com/warner/python-versioneer/issues/52 - - cmds = {} if cmdclass is None else cmdclass.copy() - - # we add "version" to both distutils and setuptools - from distutils.core import Command - - class cmd_version(Command): - description = "report generated version string" - user_options = [] - boolean_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - vers = get_versions(verbose=True) - print("Version: %s" % vers["version"]) - print(" full-revisionid: %s" % vers.get("full-revisionid")) - print(" dirty: %s" % vers.get("dirty")) - print(" date: %s" % vers.get("date")) - if vers["error"]: - print(" error: %s" % vers["error"]) - cmds["version"] = cmd_version - - # we override "build_py" in both distutils and setuptools - # - # most invocation pathways end up running build_py: - # distutils/build -> build_py - # distutils/install -> distutils/build ->.. - # setuptools/bdist_wheel -> distutils/install ->.. - # setuptools/bdist_egg -> distutils/install_lib -> build_py - # setuptools/install -> bdist_egg ->.. - # setuptools/develop -> ? - # pip install: - # copies source tree to a tempdir before running egg_info/etc - # if .git isn't copied too, 'git describe' will fail - # then does setup.py bdist_wheel, or sometimes setup.py install - # setup.py egg_info -> ? - - # we override different "build_py" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.build_py import build_py as _build_py - else: - from distutils.command.build_py import build_py as _build_py - - class cmd_build_py(_build_py): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - _build_py.run(self) - # now locate _version.py in the new build/ directory and replace - # it with an updated value - if cfg.versionfile_build: - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - cmds["build_py"] = cmd_build_py - - if "cx_Freeze" in sys.modules: # cx_freeze enabled? - from cx_Freeze.dist import build_exe as _build_exe - # nczeczulin reports that py2exe won't like the pep440-style string - # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. - # setup(console=[{ - # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION - # "product_version": versioneer.get_version(), - # ... - - class cmd_build_exe(_build_exe): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - - _build_exe.run(self) - os.unlink(target_versionfile) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - cmds["build_exe"] = cmd_build_exe - del cmds["build_py"] - - if 'py2exe' in sys.modules: # py2exe enabled? - try: - from py2exe.distutils_buildexe import py2exe as _py2exe # py3 - except ImportError: - from py2exe.build_exe import py2exe as _py2exe # py2 - - class cmd_py2exe(_py2exe): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - - _py2exe.run(self) - os.unlink(target_versionfile) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - cmds["py2exe"] = cmd_py2exe - - # we override different "sdist" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.sdist import sdist as _sdist - else: - from distutils.command.sdist import sdist as _sdist - - class cmd_sdist(_sdist): - def run(self): - versions = get_versions() - self._versioneer_generated_versions = versions - # unless we update this, the command will keep using the old - # version - self.distribution.metadata.version = versions["version"] - return _sdist.run(self) - - def make_release_tree(self, base_dir, files): - root = get_root() - cfg = get_config_from_root(root) - _sdist.make_release_tree(self, base_dir, files) - # now locate _version.py in the new base_dir directory - # (remembering that it may be a hardlink) and replace it with an - # updated value - target_versionfile = os.path.join(base_dir, cfg.versionfile_source) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, - self._versioneer_generated_versions) - cmds["sdist"] = cmd_sdist - - return cmds - - -CONFIG_ERROR = """ -setup.cfg is missing the necessary Versioneer configuration. You need -a section like: - - [versioneer] - VCS = git - style = pep440 - versionfile_source = src/myproject/_version.py - versionfile_build = myproject/_version.py - tag_prefix = - parentdir_prefix = myproject- - -You will also need to edit your setup.py to use the results: - - import versioneer - setup(version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), ...) - -Please read the docstring in ./versioneer.py for configuration instructions, -edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. -""" - -SAMPLE_CONFIG = """ -# See the docstring in versioneer.py for instructions. Note that you must -# re-run 'versioneer.py setup' after changing this section, and commit the -# resulting files. - -[versioneer] -#VCS = git -#style = pep440 -#versionfile_source = -#versionfile_build = -#tag_prefix = -#parentdir_prefix = - -""" - -INIT_PY_SNIPPET = """ -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions -""" - - -def do_setup(): - """Main VCS-independent setup function for installing Versioneer.""" - root = get_root() - try: - cfg = get_config_from_root(root) - except (EnvironmentError, configparser.NoSectionError, - configparser.NoOptionError) as e: - if isinstance(e, (EnvironmentError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", - file=sys.stderr) - with open(os.path.join(root, "setup.cfg"), "a") as f: - f.write(SAMPLE_CONFIG) - print(CONFIG_ERROR, file=sys.stderr) - return 1 - - print(" creating %s" % cfg.versionfile_source) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - - ipy = os.path.join(os.path.dirname(cfg.versionfile_source), - "__init__.py") - if os.path.exists(ipy): - try: - with open(ipy, "r") as f: - old = f.read() - except EnvironmentError: - old = "" - if INIT_PY_SNIPPET not in old: - print(" appending to %s" % ipy) - with open(ipy, "a") as f: - f.write(INIT_PY_SNIPPET) - else: - print(" %s unmodified" % ipy) - else: - print(" %s doesn't exist, ok" % ipy) - ipy = None - - # Make sure both the top-level "versioneer.py" and versionfile_source - # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so - # they'll be copied into source distributions. Pip won't be able to - # install the package without this. - manifest_in = os.path.join(root, "MANIFEST.in") - simple_includes = set() - try: - with open(manifest_in, "r") as f: - for line in f: - if line.startswith("include "): - for include in line.split()[1:]: - simple_includes.add(include) - except EnvironmentError: - pass - # That doesn't cover everything MANIFEST.in can do - # (http://docs.python.org/2/distutils/sourcedist.html#commands), so - # it might give some false negatives. Appending redundant 'include' - # lines is safe, though. - if "versioneer.py" not in simple_includes: - print(" appending 'versioneer.py' to MANIFEST.in") - with open(manifest_in, "a") as f: - f.write("include versioneer.py\n") - else: - print(" 'versioneer.py' already in MANIFEST.in") - if cfg.versionfile_source not in simple_includes: - print(" appending versionfile_source ('%s') to MANIFEST.in" % - cfg.versionfile_source) - with open(manifest_in, "a") as f: - f.write("include %s\n" % cfg.versionfile_source) - else: - print(" versionfile_source already in MANIFEST.in") - - # Make VCS-specific changes. For git, this means creating/changing - # .gitattributes to mark _version.py for export-subst keyword - # substitution. - do_vcs_install(manifest_in, cfg.versionfile_source, ipy) - return 0 - - -def scan_setup_py(): - """Validate the contents of setup.py against Versioneer's expectations.""" - found = set() - setters = False - errors = 0 - with open("setup.py", "r") as f: - for line in f.readlines(): - if "import versioneer" in line: - found.add("import") - if "versioneer.get_cmdclass()" in line: - found.add("cmdclass") - if "versioneer.get_version()" in line: - found.add("get_version") - if "versioneer.VCS" in line: - setters = True - if "versioneer.versionfile_source" in line: - setters = True - if len(found) != 3: - print("") - print("Your setup.py appears to be missing some important items") - print("(but I might be wrong). Please make sure it has something") - print("roughly like the following:") - print("") - print(" import versioneer") - print(" setup( version=versioneer.get_version(),") - print(" cmdclass=versioneer.get_cmdclass(), ...)") - print("") - errors += 1 - if setters: - print("You should remove lines like 'versioneer.VCS = ' and") - print("'versioneer.versionfile_source = ' . This configuration") - print("now lives in setup.cfg, and should be removed from setup.py") - print("") - errors += 1 - return errors - - -if __name__ == "__main__": - cmd = sys.argv[1] - if cmd == "setup": - errors = do_setup() - errors += scan_setup_py() - if errors: - sys.exit(1)